@mindline/sync 1.0.109 → 1.0.110
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 +36 -5
- package/dist/sync.es.js +787 -722
- package/dist/sync.es.js.map +1 -1
- package/dist/sync.umd.js +42 -42
- package/dist/sync.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/index.d.ts +8 -3
- package/src/index.ts +263 -88
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,31 @@ 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
|
+
// Last known backend queues (strongest completion signal)
|
|
933
|
+
private lastQueues: { main: number | null; writer: number | null; deferred: number | null } = {
|
|
934
|
+
main: null,
|
|
935
|
+
writer: null,
|
|
936
|
+
deferred: null
|
|
937
|
+
};
|
|
938
|
+
private hasQueueInfo: boolean = false;
|
|
939
|
+
|
|
940
|
+
// Store UI callbacks so we can update the UI from poll/completion events.
|
|
941
|
+
private setIdleText: ((idleText: string) => void) | null = null;
|
|
942
|
+
|
|
943
|
+
// Handler used by polling: we reuse the same stats-processing logic.
|
|
944
|
+
private statsHydrationHandler: ((batchId: string, stats: any) => void) | null = null;
|
|
926
945
|
milestoneArray: MilestoneArray;
|
|
927
946
|
constructor(
|
|
928
947
|
config: SyncConfig | null,
|
|
@@ -933,16 +952,16 @@ export class BatchArray {
|
|
|
933
952
|
this.init(config, syncPortalGlobalState, bClearLocalStorage);
|
|
934
953
|
this.pb_startTS = 0;
|
|
935
954
|
this.pb_progress = 0;
|
|
936
|
-
this.pb_increment = .25;
|
|
937
955
|
this.pb_timer = null;
|
|
938
956
|
this.pb_idle = 0;
|
|
939
|
-
this.pb_idleMax = 0;
|
|
940
957
|
this.pb_total = 0;
|
|
941
958
|
this.milestoneArray = new MilestoneArray(false);
|
|
942
959
|
}
|
|
943
960
|
clearStoredBatchIds(): void {
|
|
944
961
|
if (storageAvailable()) {
|
|
945
962
|
localStorage.setItem("BatchIdArray", "[]");
|
|
963
|
+
// Also clear any persisted UI progress for the in-flight batch restore.
|
|
964
|
+
localStorage.removeItem("BatchIdArrayProgress");
|
|
946
965
|
}
|
|
947
966
|
}
|
|
948
967
|
// populate tenantNodes based on config tenants
|
|
@@ -1026,59 +1045,141 @@ export class BatchArray {
|
|
|
1026
1045
|
});
|
|
1027
1046
|
}
|
|
1028
1047
|
}
|
|
1029
|
-
initializeProgressBar(setSyncProgress: (progress: number) => void,
|
|
1048
|
+
initializeProgressBar(setSyncProgress: (progress: number) => void, _setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): void {
|
|
1049
|
+
// Save callback so completion/polling can update the idle text too.
|
|
1050
|
+
this.setIdleText = setIdleText;
|
|
1051
|
+
|
|
1052
|
+
// IMPORTANT: reset completion signals between sync runs.
|
|
1053
|
+
// Otherwise a previous run that ended with queues=0 could make the next run immediately "Complete".
|
|
1054
|
+
this.hasQueueInfo = false;
|
|
1055
|
+
this.lastQueues = { main: null, writer: null, deferred: null };
|
|
1056
|
+
|
|
1030
1057
|
this.pb_startTS = Date.now();
|
|
1031
1058
|
this.pb_progress = 0;
|
|
1032
|
-
this.pb_increment = .25;
|
|
1033
1059
|
this.pb_idle = 0;
|
|
1034
|
-
this.pb_idleMax = 0;
|
|
1035
1060
|
this.pb_total = 0;
|
|
1061
|
+
setSyncProgress(this.pb_progress);
|
|
1062
|
+
setIdleText("Starting sync...");
|
|
1036
1063
|
this.pb_timer = setInterval(() => {
|
|
1037
|
-
//
|
|
1064
|
+
// If backend indicates completion (prefer queue-empty when available), stop the timer
|
|
1038
1065
|
console.log("this.tenantNodes", this.tenantNodes)
|
|
1039
|
-
let isCompletedOrNothingToSync = this.tenantNodes.
|
|
1040
|
-
|
|
1066
|
+
let isCompletedOrNothingToSync = this.tenantNodes.every((tn: TenantNode) =>
|
|
1067
|
+
tn.nothingtosync || tn.targets.every((ta) => ta.status === "complete" || ta.status === "failed")
|
|
1068
|
+
);
|
|
1069
|
+
|
|
1070
|
+
const queuesEmpty = this.hasQueueInfo &&
|
|
1071
|
+
this.lastQueues.main === 0 &&
|
|
1072
|
+
this.lastQueues.writer === 0 &&
|
|
1073
|
+
this.lastQueues.deferred === 0;
|
|
1074
|
+
|
|
1075
|
+
// If we have queue info, trust it for completion. Otherwise fall back to node terminal states.
|
|
1076
|
+
const isCompleted = queuesEmpty || (!this.hasQueueInfo && isCompletedOrNothingToSync);
|
|
1077
|
+
|
|
1078
|
+
if (isCompleted) {
|
|
1041
1079
|
clearInterval(this.pb_timer!);
|
|
1042
1080
|
this.pb_timer = null;
|
|
1043
1081
|
this.pb_progress = 100;
|
|
1044
1082
|
setSyncProgress(this.pb_progress);
|
|
1045
|
-
setIdleText(`Complete
|
|
1083
|
+
setIdleText(`Complete.`);
|
|
1084
|
+
this.stopPolling();
|
|
1046
1085
|
this.clearStoredBatchIds();
|
|
1047
1086
|
}
|
|
1048
1087
|
else {
|
|
1049
|
-
// if we've gone 60 seconds without a signalR message, finish the sync
|
|
1050
1088
|
this.pb_total = this.pb_total + 1;
|
|
1051
1089
|
this.pb_idle = this.pb_idle + 1;
|
|
1052
|
-
|
|
1053
|
-
setIdleText(`${this.pb_total} seconds elapsed. Last update ${this.pb_idle} seconds ago
|
|
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
|
-
}
|
|
1090
|
+
|
|
1091
|
+
setIdleText(`${this.pb_total} seconds elapsed. Last update ${this.pb_idle} seconds ago.`);
|
|
1065
1092
|
}
|
|
1066
1093
|
}, 1000);
|
|
1067
1094
|
this.milestoneArray.start(setMilestones);
|
|
1068
1095
|
}
|
|
1096
|
+
|
|
1097
|
+
private stopPolling(): void {
|
|
1098
|
+
if (this.pollTimer != null) {
|
|
1099
|
+
clearInterval(this.pollTimer);
|
|
1100
|
+
this.pollTimer = null;
|
|
1101
|
+
}
|
|
1102
|
+
this.pollInstance = null;
|
|
1103
|
+
this.pollAuthorizedUser = null;
|
|
1104
|
+
this.pollBatchIdArray = [];
|
|
1105
|
+
this.pollLastUpdatedByBatchId = {};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
private async pollStatsOnce(): Promise<void> {
|
|
1109
|
+
// Polling-only mode (single source of truth: /api/stats).
|
|
1110
|
+
if (this.pb_timer == null) return;
|
|
1111
|
+
if (this.pollInstance == null || this.pollAuthorizedUser == null) return;
|
|
1112
|
+
if (!this.statsHydrationHandler) return;
|
|
1113
|
+
if (!this.pollBatchIdArray || this.pollBatchIdArray.length === 0) return;
|
|
1114
|
+
|
|
1115
|
+
for (const batchPair of this.pollBatchIdArray) {
|
|
1116
|
+
const batchId: string | undefined = batchPair?.BatchId;
|
|
1117
|
+
if (!batchId) continue;
|
|
1118
|
+
|
|
1119
|
+
const statsResult: APIResult = await readerStats(this.pollInstance, this.pollAuthorizedUser, batchId);
|
|
1120
|
+
if (!statsResult.result || !statsResult.array || !statsResult.array[0]) continue;
|
|
1121
|
+
|
|
1122
|
+
const payload: any = statsResult.array[0];
|
|
1123
|
+
const lastUpdated: string | undefined = payload.lastUpdated;
|
|
1124
|
+
const prevLastUpdated = this.pollLastUpdatedByBatchId[batchId];
|
|
1125
|
+
this.pollLastUpdatedByBatchId[batchId] = lastUpdated ?? "";
|
|
1126
|
+
|
|
1127
|
+
// Only feed the handler if backend state changed.
|
|
1128
|
+
if (!lastUpdated || lastUpdated === prevLastUpdated) continue;
|
|
1129
|
+
|
|
1130
|
+
this.statsHydrationHandler(batchId, payload.stats);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1069
1133
|
uninitializeProgressBar(setSyncProgress: (progress: number) => void, setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): void {
|
|
1070
1134
|
this.pb_startTS = 0;
|
|
1071
1135
|
this.pb_progress = 0;
|
|
1072
1136
|
setSyncProgress(this.pb_progress);
|
|
1073
1137
|
setConfigSyncResult("sync failed to execute");
|
|
1074
|
-
this.pb_increment = 0;
|
|
1075
1138
|
clearInterval(this.pb_timer!);
|
|
1076
1139
|
this.pb_timer = null;
|
|
1077
1140
|
this.pb_idle = 0;
|
|
1078
|
-
this.
|
|
1079
|
-
setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}]`);
|
|
1141
|
+
setIdleText(`No updates seen for ${this.pb_idle} seconds.`);
|
|
1080
1142
|
this.milestoneArray.unstart(setMilestones);
|
|
1081
1143
|
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Calculates the sync progress percentage (0-99%) based on completed read and write operations.
|
|
1146
|
+
*
|
|
1147
|
+
* The progress formula is: (completedReads + completedWrites) / (total * 2) * 100
|
|
1148
|
+
*
|
|
1149
|
+
* Progress is divided by (total * 2) because the sync process has two phases:
|
|
1150
|
+
* 1. Reading phase: read users from source + excluded users
|
|
1151
|
+
* 2. Writing phase: write users to target + excluded users + deferred users
|
|
1152
|
+
*
|
|
1153
|
+
* Each phase can contribute up to 50% of the total progress. The result is capped at 99%
|
|
1154
|
+
* to prevent premature completion (100% is only shown when sync is truly complete).
|
|
1155
|
+
*
|
|
1156
|
+
* @param total - Total number of users to sync
|
|
1157
|
+
* @param read - Number of users read from source
|
|
1158
|
+
* @param written - Number of users written to target
|
|
1159
|
+
* @param excluded - Number of users excluded from sync
|
|
1160
|
+
* @param deferred - Number of users deferred (postponed) during writing
|
|
1161
|
+
* @returns Progress percentage from 0 to 99
|
|
1162
|
+
*/
|
|
1163
|
+
calculateProgress(total: number, read: number, written: number, excluded: number, deferred: number): number {
|
|
1164
|
+
if (total <= 0) {
|
|
1165
|
+
return 0;
|
|
1166
|
+
}
|
|
1167
|
+
const completedReads = Math.min(total, read + excluded);
|
|
1168
|
+
const completedWrites = Math.min(total, written + excluded + deferred);
|
|
1169
|
+
const completedUnits = completedReads + completedWrites;
|
|
1170
|
+
return Math.min(99, Math.round((completedUnits / (total * 2)) * 100));
|
|
1171
|
+
}
|
|
1172
|
+
updateProgressFromTotals(total: number, read: number, written: number, excluded: number, deferred: number, setSyncProgress: (progress: number) => void): void {
|
|
1173
|
+
const progress = this.calculateProgress(total, read, written, excluded, deferred);
|
|
1174
|
+
this.pb_progress = Math.max(this.pb_progress, progress);
|
|
1175
|
+
setSyncProgress(this.pb_progress);
|
|
1176
|
+
|
|
1177
|
+
// Persist progress so a page refresh can restore the same (or higher) percent.
|
|
1178
|
+
// Note: This is intentionally monotonic because pb_progress is monotonic.
|
|
1179
|
+
if (storageAvailable()) {
|
|
1180
|
+
localStorage.setItem("BatchIdArrayProgress", String(this.pb_progress));
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1082
1183
|
initializeSignalR(
|
|
1083
1184
|
config: SyncConfig | null | undefined,
|
|
1084
1185
|
syncPortalGlobalState: InitInfo | null,
|
|
@@ -1092,25 +1193,32 @@ export class BatchArray {
|
|
|
1092
1193
|
setWritersCurrent: (writersCurrent: number) => void,
|
|
1093
1194
|
setMilestones: (milestones: Milestone[]) => void,
|
|
1094
1195
|
setConfigSyncResult: (result: string) => void,
|
|
1196
|
+
setSyncProgress: (progress: number) => void,
|
|
1095
1197
|
bClearLocalStorage: boolean,
|
|
1096
|
-
message: string
|
|
1198
|
+
message: string,
|
|
1199
|
+
instance?: IPublicClientApplication,
|
|
1200
|
+
authorizedUser?: User
|
|
1097
1201
|
): void {
|
|
1098
1202
|
bClearLocalStorage = bClearLocalStorage;
|
|
1203
|
+
message = message;
|
|
1204
|
+
|
|
1205
|
+
// Starting a new SignalR session for a (potentially new) run: reset queue-based completion signals.
|
|
1206
|
+
this.hasQueueInfo = false;
|
|
1207
|
+
this.lastQueues = { main: null, writer: null, deferred: null };
|
|
1208
|
+
|
|
1099
1209
|
// we have just completed a successful POST to startSync
|
|
1100
1210
|
this.milestoneArray.post(setMilestones);
|
|
1101
1211
|
setConfigSyncResult("started sync, waiting for updates...");
|
|
1102
1212
|
// re-initialize batch array with Configuration updated by the succcessful POST to startSync
|
|
1103
1213
|
this.init(config, syncPortalGlobalState, false);
|
|
1104
|
-
// define
|
|
1105
|
-
let handler = (
|
|
1106
|
-
console.log(message);
|
|
1107
|
-
let item = JSON.parse(message);
|
|
1214
|
+
// define a stats hydration handler (used by polling)
|
|
1215
|
+
let handler = (batchId: string, statsarray: any) => {
|
|
1108
1216
|
// reset the countdown timer every time we get a message
|
|
1109
1217
|
this.pb_idle = 0;
|
|
1110
|
-
// find the associated tenant for this
|
|
1111
|
-
let matchingPair: any | undefined = batchIdArray.find((o: any) => o.BatchId ==
|
|
1218
|
+
// find the associated tenant for this batchId
|
|
1219
|
+
let matchingPair: any | undefined = batchIdArray.find((o: any) => o.BatchId == batchId);
|
|
1112
1220
|
if (matchingPair == null) {
|
|
1113
|
-
console.log(`Batch ${
|
|
1221
|
+
console.log(`Batch ${batchId} not found in batchIdArray.`);
|
|
1114
1222
|
debugger;
|
|
1115
1223
|
return;
|
|
1116
1224
|
}
|
|
@@ -1121,13 +1229,16 @@ export class BatchArray {
|
|
|
1121
1229
|
return;
|
|
1122
1230
|
}
|
|
1123
1231
|
tenantNode.batchId = matchingPair.BatchId;
|
|
1124
|
-
// process stats for this
|
|
1125
|
-
let statsarray = item.Stats; // get the array of statistics
|
|
1232
|
+
// process stats for this batch (one batch per tenant node)
|
|
1126
1233
|
let statskeys = Object.keys(statsarray); // get the keys of the array
|
|
1127
1234
|
let statsvalues = Object.values(statsarray); // get the values of the array
|
|
1128
1235
|
// does this tenantnode/batch have nothing to sync?
|
|
1129
1236
|
let bTotalCountZero: boolean = false;
|
|
1130
1237
|
let bCurrentCountZero: boolean = false;
|
|
1238
|
+
// queue stats (better completion signal than only written/total)
|
|
1239
|
+
let totalInMainQueue: number | null = null;
|
|
1240
|
+
let totalInWriterQueue: number | null = null;
|
|
1241
|
+
let totalInDeferredQueue: number | null = null;
|
|
1131
1242
|
for (let j = 0; j < statskeys.length; j++) {
|
|
1132
1243
|
let bTotalCount = statskeys[j].endsWith("TotalCount");
|
|
1133
1244
|
let bCurrentCount = statskeys[j].endsWith("CurrentCount");
|
|
@@ -1180,9 +1291,21 @@ export class BatchArray {
|
|
|
1180
1291
|
}
|
|
1181
1292
|
}
|
|
1182
1293
|
tenantNode.nothingtosync = bTotalCountZero && bCurrentCountZero;
|
|
1294
|
+
|
|
1295
|
+
// queue stats are global for the batch
|
|
1296
|
+
if (statskeys[j] === "TotalInMainQueue") {
|
|
1297
|
+
totalInMainQueue = Number(statsvalues[j]);
|
|
1298
|
+
}
|
|
1299
|
+
if (statskeys[j] === "TotalInWriterQueue") {
|
|
1300
|
+
totalInWriterQueue = Number(statsvalues[j]);
|
|
1301
|
+
}
|
|
1302
|
+
if (statskeys[j] === "TotalInDeferredQueue") {
|
|
1303
|
+
totalInDeferredQueue = Number(statsvalues[j]);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1183
1306
|
if (statskeys[j].startsWith("Writer")) {
|
|
1184
1307
|
// parse tid from Writer key
|
|
1185
|
-
let tidRegexp = /
|
|
1308
|
+
let tidRegexp = /Writer\/TID:(.+)\/TotalCount/;
|
|
1186
1309
|
if (bCurrentCount) tidRegexp = /Writer\/TID:(.+)\/CurrentCount/;
|
|
1187
1310
|
if (bExcludedCount) tidRegexp = /Writer\/TID:(.+)\/ExtCount/;
|
|
1188
1311
|
if (bDeferredCount) tidRegexp = /Writer\/TID:(.+)\/DeferredCount/;
|
|
@@ -1204,7 +1327,7 @@ export class BatchArray {
|
|
|
1204
1327
|
writerNode.total = Math.max(Number(tenantNode.total), writerNode.total);
|
|
1205
1328
|
writerNode.batchId = matchingPair.BatchId;
|
|
1206
1329
|
if (bTotalCount) {
|
|
1207
|
-
writerNode.total = Math.max(Number(
|
|
1330
|
+
writerNode.total = Math.max(Number(statsvalues[j]), writerNode.total);
|
|
1208
1331
|
console.log(`----- ${writerNode.name} TID: ${writerNode.tid} batchId: ${writerNode.batchId}`);
|
|
1209
1332
|
console.log(`----- ${writerNode.name} Total To Write: ${writerNode.total}`);
|
|
1210
1333
|
}
|
|
@@ -1238,6 +1361,7 @@ export class BatchArray {
|
|
|
1238
1361
|
let writerTotal: number = 0;
|
|
1239
1362
|
let writerCurrent: number = 0;
|
|
1240
1363
|
let writerExcluded: number = 0;
|
|
1364
|
+
let writerDeferred: number = 0;
|
|
1241
1365
|
this.tenantNodes.map((sourceTenantNode: TenantNode) => {
|
|
1242
1366
|
sourceTenantNode.targets.map((writerNode: TenantNode) => {
|
|
1243
1367
|
bWritingComplete &&= (writerNode.status == "complete" || writerNode.status == "failed");
|
|
@@ -1245,13 +1369,40 @@ export class BatchArray {
|
|
|
1245
1369
|
writerTotal += Math.max(writerNode.total, sourceTenantNode.total);
|
|
1246
1370
|
writerCurrent += writerNode.written;
|
|
1247
1371
|
writerExcluded += writerNode.excluded;
|
|
1372
|
+
writerDeferred += writerNode.deferred;
|
|
1248
1373
|
});
|
|
1249
1374
|
bNothingToSync &&= sourceTenantNode.nothingtosync;
|
|
1250
|
-
|
|
1375
|
+
// Reading completion should be derived from read/excluded totals, not from source status.
|
|
1376
|
+
bReadingComplete &&= (sourceTenantNode.total > 0 && (sourceTenantNode.read + sourceTenantNode.excluded) >= sourceTenantNode.total);
|
|
1251
1377
|
readerTotal += sourceTenantNode.total;
|
|
1252
1378
|
readerCurrent += sourceTenantNode.read;
|
|
1253
1379
|
readerExcluded += sourceTenantNode.excluded;
|
|
1254
1380
|
});
|
|
1381
|
+
|
|
1382
|
+
// If queue stats are available, they are a stronger indicator of write completion.
|
|
1383
|
+
// When all queues are empty, the backend has finished processing this batch and may stop emitting updates.
|
|
1384
|
+
if (totalInMainQueue != null) this.lastQueues.main = totalInMainQueue;
|
|
1385
|
+
if (totalInWriterQueue != null) this.lastQueues.writer = totalInWriterQueue;
|
|
1386
|
+
if (totalInDeferredQueue != null) this.lastQueues.deferred = totalInDeferredQueue;
|
|
1387
|
+
this.hasQueueInfo = this.lastQueues.main != null && this.lastQueues.writer != null && this.lastQueues.deferred != null;
|
|
1388
|
+
|
|
1389
|
+
const queuesEmpty = this.hasQueueInfo &&
|
|
1390
|
+
this.lastQueues.main === 0 &&
|
|
1391
|
+
this.lastQueues.writer === 0 &&
|
|
1392
|
+
this.lastQueues.deferred === 0;
|
|
1393
|
+
|
|
1394
|
+
if (queuesEmpty) {
|
|
1395
|
+
bWritingComplete = true;
|
|
1396
|
+
|
|
1397
|
+
// Ensure the UI can reach a terminal state even if writer CurrentCount never reaches TotalCount.
|
|
1398
|
+
// We trust the backend queues more than client-side totals in this scenario.
|
|
1399
|
+
this.tenantNodes.forEach((src) => {
|
|
1400
|
+
src.targets.forEach((t) => {
|
|
1401
|
+
if (t.status !== "failed") t.status = "complete";
|
|
1402
|
+
});
|
|
1403
|
+
if (src.status !== "failed") src.status = "complete";
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1255
1406
|
// set linear gauge max and current values
|
|
1256
1407
|
setReadersTotal(readerTotal);
|
|
1257
1408
|
setReadersCurrent(readerCurrent);
|
|
@@ -1259,11 +1410,33 @@ export class BatchArray {
|
|
|
1259
1410
|
setWritersTotal(Math.max(writerTotal, readerTotal));
|
|
1260
1411
|
setWritersCurrent(writerCurrent);
|
|
1261
1412
|
setWritersExcluded(writerExcluded);
|
|
1413
|
+
|
|
1414
|
+
// update progress bar based on backend totals (monotonic)
|
|
1415
|
+
// This avoids progress decreasing after refresh when local timer state restarts.
|
|
1416
|
+
// Excluded users count as completed for both read and write.
|
|
1417
|
+
// Deferred users count as completed for write.
|
|
1418
|
+
this.updateProgressFromTotals(
|
|
1419
|
+
Math.max(readerTotal, writerTotal),
|
|
1420
|
+
readerCurrent,
|
|
1421
|
+
writerCurrent,
|
|
1422
|
+
Math.max(readerExcluded, writerExcluded),
|
|
1423
|
+
// Deferred users count as completed for write.
|
|
1424
|
+
writerDeferred,
|
|
1425
|
+
setSyncProgress
|
|
1426
|
+
);
|
|
1262
1427
|
// check to see if there was nothing to sync
|
|
1263
1428
|
if (bNothingToSync) {
|
|
1264
1429
|
this.milestoneArray.write(setMilestones);
|
|
1265
|
-
|
|
1430
|
+
this.stopPolling();
|
|
1266
1431
|
setConfigSyncResult("nothing to sync");
|
|
1432
|
+
// force completion visuals
|
|
1433
|
+
if (this.pb_timer) {
|
|
1434
|
+
clearInterval(this.pb_timer);
|
|
1435
|
+
this.pb_timer = null;
|
|
1436
|
+
}
|
|
1437
|
+
this.pb_progress = 100;
|
|
1438
|
+
setSyncProgress(this.pb_progress);
|
|
1439
|
+
this.setIdleText?.(`Complete (nothing to sync).`);
|
|
1267
1440
|
this.clearStoredBatchIds();
|
|
1268
1441
|
console.log(`Setting config sync result: "nothing to sync"`);
|
|
1269
1442
|
}
|
|
@@ -1275,19 +1448,21 @@ export class BatchArray {
|
|
|
1275
1448
|
console.log(`Setting config sync result: "reading complete"`);
|
|
1276
1449
|
// trigger refresh delta tokens
|
|
1277
1450
|
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
1451
|
}
|
|
1286
1452
|
// with that out of the way, is writing complete?
|
|
1287
|
-
if
|
|
1453
|
+
// Only allow terminal completion when backend queues are empty (if we have queue info).
|
|
1454
|
+
if (bWritingComplete && (queuesEmpty || !this.hasQueueInfo)) {
|
|
1288
1455
|
this.milestoneArray.write(setMilestones);
|
|
1289
|
-
|
|
1456
|
+
this.stopPolling();
|
|
1290
1457
|
setConfigSyncResult("sync complete");
|
|
1458
|
+
// force completion visuals
|
|
1459
|
+
if (this.pb_timer) {
|
|
1460
|
+
clearInterval(this.pb_timer);
|
|
1461
|
+
this.pb_timer = null;
|
|
1462
|
+
}
|
|
1463
|
+
this.pb_progress = 100;
|
|
1464
|
+
setSyncProgress(this.pb_progress);
|
|
1465
|
+
this.setIdleText?.(`Complete.`);
|
|
1291
1466
|
this.clearStoredBatchIds();
|
|
1292
1467
|
console.log(`Setting config sync result: "complete"`);
|
|
1293
1468
|
}
|
|
@@ -1304,38 +1479,26 @@ export class BatchArray {
|
|
|
1304
1479
|
}
|
|
1305
1480
|
}
|
|
1306
1481
|
|
|
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
|
-
});
|
|
1482
|
+
// allow polling to feed the same handler
|
|
1483
|
+
this.statsHydrationHandler = handler;
|
|
1484
|
+
|
|
1485
|
+
// Start polling (only if caller provided credentials)
|
|
1486
|
+
if (instance && authorizedUser) {
|
|
1487
|
+
this.pollInstance = instance;
|
|
1488
|
+
this.pollAuthorizedUser = authorizedUser;
|
|
1489
|
+
this.pollBatchIdArray = batchIdArray as any[];
|
|
1490
|
+
this.stopPolling(); // clear any previous poller state
|
|
1491
|
+
this.pollInstance = instance;
|
|
1492
|
+
this.pollAuthorizedUser = authorizedUser;
|
|
1493
|
+
this.pollBatchIdArray = batchIdArray as any[];
|
|
1494
|
+
|
|
1495
|
+
// Hydrate immediately (no need to wait pollIntervalSeconds)
|
|
1496
|
+
void this.pollStatsOnce();
|
|
1497
|
+
|
|
1498
|
+
this.pollTimer = setInterval(() => {
|
|
1499
|
+
void this.pollStatsOnce();
|
|
1500
|
+
}, this.pollIntervalSeconds * 1000);
|
|
1501
|
+
}
|
|
1339
1502
|
}
|
|
1340
1503
|
// start a sync cycle
|
|
1341
1504
|
async startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: SyncConfig | null | undefined): Promise<APIResult> {
|
|
@@ -1383,8 +1546,20 @@ export class TenantNode {
|
|
|
1383
1546
|
this.deferred = deferred;
|
|
1384
1547
|
if (this.read === 0 && this.written === 0) this.status = "not started";
|
|
1385
1548
|
if (this.read > 0) {
|
|
1386
|
-
if (this.read + this.excluded < this.total)
|
|
1387
|
-
|
|
1549
|
+
if (this.read + this.excluded < this.total) {
|
|
1550
|
+
this.status = "in progress";
|
|
1551
|
+
}
|
|
1552
|
+
else if (this.read + this.excluded === this.total) {
|
|
1553
|
+
// For source nodes (nodes with targets), reading complete doesn't mean the whole branch is complete.
|
|
1554
|
+
// Avoid reporting "complete" while any target is still running.
|
|
1555
|
+
if (this.targets != null && this.targets.length > 0) {
|
|
1556
|
+
const allTargetsTerminal = this.targets.every(t => t.status === "complete" || t.status === "failed");
|
|
1557
|
+
this.status = allTargetsTerminal ? "complete" : "in progress";
|
|
1558
|
+
}
|
|
1559
|
+
else {
|
|
1560
|
+
this.status = "complete";
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1388
1563
|
}
|
|
1389
1564
|
else if (this.written > 0) {
|
|
1390
1565
|
if (this.written + this.deferred + this.excluded < this.total) this.status = "in progress";
|