@mindline/sync 1.0.108 → 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/.vs/VSWorkspaceState.json +1 -0
- package/.vs/slnx.sqlite +0 -0
- package/.vs/sync.slnx/FileContentIndex/{13d6a28d-932f-4eb6-818c-efea35f24838.vsidx → 233f16f5-9502-4eee-892d-94508f320b43.vsidx} +0 -0
- package/.vs/sync.slnx/FileContentIndex/88b226f1-9afd-4cf3-bdb6-5db742bb7e8d.vsidx +0 -0
- package/.vs/sync.slnx/FileContentIndex/f109c15d-d422-45e9-a5df-0b391ae0a643.vsidx +0 -0
- package/.vs/sync.slnx/v18/.wsuo +0 -0
- package/.vs/sync.slnx/v18/DocumentLayout.backup.json +20 -3
- package/.vs/sync.slnx/v18/DocumentLayout.json +35 -2
- package/dist/src/index.d.ts +37 -5
- package/dist/sync.es.js +986 -918
- package/dist/sync.es.js.map +1 -1
- package/dist/sync.umd.js +44 -44
- package/dist/sync.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/index.d.ts +8 -3
- package/src/index.ts +275 -90
- package/.vs/sync.slnx/FileContentIndex/75bd0095-9fc3-4f35-90e9-f4022b8e4b55.vsidx +0 -0
- package/.vs/sync.slnx/FileContentIndex/92b75297-fe76-448d-80d4-cbf5ef4fcd60.vsidx +0 -0
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,13 +952,18 @@ 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 = 0;
|
|
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
|
}
|
|
960
|
+
clearStoredBatchIds(): void {
|
|
961
|
+
if (storageAvailable()) {
|
|
962
|
+
localStorage.setItem("BatchIdArray", "[]");
|
|
963
|
+
// Also clear any persisted UI progress for the in-flight batch restore.
|
|
964
|
+
localStorage.removeItem("BatchIdArrayProgress");
|
|
965
|
+
}
|
|
966
|
+
}
|
|
943
967
|
// populate tenantNodes based on config tenants
|
|
944
968
|
init(
|
|
945
969
|
config: SyncConfig | null | undefined,
|
|
@@ -1021,58 +1045,141 @@ export class BatchArray {
|
|
|
1021
1045
|
});
|
|
1022
1046
|
}
|
|
1023
1047
|
}
|
|
1024
|
-
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
|
+
|
|
1025
1057
|
this.pb_startTS = Date.now();
|
|
1026
1058
|
this.pb_progress = 0;
|
|
1027
|
-
this.pb_increment = .25;
|
|
1028
1059
|
this.pb_idle = 0;
|
|
1029
|
-
this.pb_idleMax = 0;
|
|
1030
1060
|
this.pb_total = 0;
|
|
1061
|
+
setSyncProgress(this.pb_progress);
|
|
1062
|
+
setIdleText("Starting sync...");
|
|
1031
1063
|
this.pb_timer = setInterval(() => {
|
|
1032
|
-
//
|
|
1064
|
+
// If backend indicates completion (prefer queue-empty when available), stop the timer
|
|
1033
1065
|
console.log("this.tenantNodes", this.tenantNodes)
|
|
1034
|
-
let isCompletedOrNothingToSync = this.tenantNodes.
|
|
1035
|
-
|
|
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) {
|
|
1036
1079
|
clearInterval(this.pb_timer!);
|
|
1037
1080
|
this.pb_timer = null;
|
|
1038
1081
|
this.pb_progress = 100;
|
|
1039
1082
|
setSyncProgress(this.pb_progress);
|
|
1040
|
-
setIdleText(`Complete
|
|
1083
|
+
setIdleText(`Complete.`);
|
|
1084
|
+
this.stopPolling();
|
|
1085
|
+
this.clearStoredBatchIds();
|
|
1041
1086
|
}
|
|
1042
1087
|
else {
|
|
1043
|
-
// if we've gone 60 seconds without a signalR message, finish the sync
|
|
1044
1088
|
this.pb_total = this.pb_total + 1;
|
|
1045
1089
|
this.pb_idle = this.pb_idle + 1;
|
|
1046
|
-
|
|
1047
|
-
setIdleText(`${this.pb_total} seconds elapsed. Last update ${this.pb_idle} seconds ago
|
|
1048
|
-
if (this.pb_idle >= 60) {
|
|
1049
|
-
if (this.milestoneArray.milestones[0].Write == null) {
|
|
1050
|
-
//this.milestoneArray.write(setMilestones); -- allow sync to cntinue
|
|
1051
|
-
setConfigSyncResult(`sync continuing, but no update for ${this.pb_idle} seconds`);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
// if we get to 100, the progress bar stops but SignalR or countdown timer completes the sync
|
|
1055
|
-
if (this.pb_progress < 100) {
|
|
1056
|
-
this.pb_progress = Math.min(100, this.pb_progress + this.pb_increment);
|
|
1057
|
-
setSyncProgress(this.pb_progress);
|
|
1058
|
-
}
|
|
1090
|
+
|
|
1091
|
+
setIdleText(`${this.pb_total} seconds elapsed. Last update ${this.pb_idle} seconds ago.`);
|
|
1059
1092
|
}
|
|
1060
1093
|
}, 1000);
|
|
1061
1094
|
this.milestoneArray.start(setMilestones);
|
|
1062
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
|
+
}
|
|
1063
1133
|
uninitializeProgressBar(setSyncProgress: (progress: number) => void, setConfigSyncResult: (result: string) => void, setIdleText: (idleText: string) => void, setMilestones: (milestones: Milestone[]) => void): void {
|
|
1064
1134
|
this.pb_startTS = 0;
|
|
1065
1135
|
this.pb_progress = 0;
|
|
1066
1136
|
setSyncProgress(this.pb_progress);
|
|
1067
1137
|
setConfigSyncResult("sync failed to execute");
|
|
1068
|
-
this.pb_increment = 0;
|
|
1069
1138
|
clearInterval(this.pb_timer!);
|
|
1070
1139
|
this.pb_timer = null;
|
|
1071
1140
|
this.pb_idle = 0;
|
|
1072
|
-
this.
|
|
1073
|
-
setIdleText(`No updates seen for ${this.pb_idle} seconds. [max idle: ${this.pb_idleMax}]`);
|
|
1141
|
+
setIdleText(`No updates seen for ${this.pb_idle} seconds.`);
|
|
1074
1142
|
this.milestoneArray.unstart(setMilestones);
|
|
1075
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
|
+
}
|
|
1076
1183
|
initializeSignalR(
|
|
1077
1184
|
config: SyncConfig | null | undefined,
|
|
1078
1185
|
syncPortalGlobalState: InitInfo | null,
|
|
@@ -1086,25 +1193,32 @@ export class BatchArray {
|
|
|
1086
1193
|
setWritersCurrent: (writersCurrent: number) => void,
|
|
1087
1194
|
setMilestones: (milestones: Milestone[]) => void,
|
|
1088
1195
|
setConfigSyncResult: (result: string) => void,
|
|
1196
|
+
setSyncProgress: (progress: number) => void,
|
|
1089
1197
|
bClearLocalStorage: boolean,
|
|
1090
|
-
message: string
|
|
1198
|
+
message: string,
|
|
1199
|
+
instance?: IPublicClientApplication,
|
|
1200
|
+
authorizedUser?: User
|
|
1091
1201
|
): void {
|
|
1092
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
|
+
|
|
1093
1209
|
// we have just completed a successful POST to startSync
|
|
1094
1210
|
this.milestoneArray.post(setMilestones);
|
|
1095
1211
|
setConfigSyncResult("started sync, waiting for updates...");
|
|
1096
1212
|
// re-initialize batch array with Configuration updated by the succcessful POST to startSync
|
|
1097
1213
|
this.init(config, syncPortalGlobalState, false);
|
|
1098
|
-
// define
|
|
1099
|
-
let handler = (
|
|
1100
|
-
console.log(message);
|
|
1101
|
-
let item = JSON.parse(message);
|
|
1214
|
+
// define a stats hydration handler (used by polling)
|
|
1215
|
+
let handler = (batchId: string, statsarray: any) => {
|
|
1102
1216
|
// reset the countdown timer every time we get a message
|
|
1103
1217
|
this.pb_idle = 0;
|
|
1104
|
-
// find the associated tenant for this
|
|
1105
|
-
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);
|
|
1106
1220
|
if (matchingPair == null) {
|
|
1107
|
-
console.log(`Batch ${
|
|
1221
|
+
console.log(`Batch ${batchId} not found in batchIdArray.`);
|
|
1108
1222
|
debugger;
|
|
1109
1223
|
return;
|
|
1110
1224
|
}
|
|
@@ -1115,13 +1229,16 @@ export class BatchArray {
|
|
|
1115
1229
|
return;
|
|
1116
1230
|
}
|
|
1117
1231
|
tenantNode.batchId = matchingPair.BatchId;
|
|
1118
|
-
// process stats for this
|
|
1119
|
-
let statsarray = item.Stats; // get the array of statistics
|
|
1232
|
+
// process stats for this batch (one batch per tenant node)
|
|
1120
1233
|
let statskeys = Object.keys(statsarray); // get the keys of the array
|
|
1121
1234
|
let statsvalues = Object.values(statsarray); // get the values of the array
|
|
1122
1235
|
// does this tenantnode/batch have nothing to sync?
|
|
1123
1236
|
let bTotalCountZero: boolean = false;
|
|
1124
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;
|
|
1125
1242
|
for (let j = 0; j < statskeys.length; j++) {
|
|
1126
1243
|
let bTotalCount = statskeys[j].endsWith("TotalCount");
|
|
1127
1244
|
let bCurrentCount = statskeys[j].endsWith("CurrentCount");
|
|
@@ -1174,9 +1291,21 @@ export class BatchArray {
|
|
|
1174
1291
|
}
|
|
1175
1292
|
}
|
|
1176
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
|
+
|
|
1177
1306
|
if (statskeys[j].startsWith("Writer")) {
|
|
1178
1307
|
// parse tid from Writer key
|
|
1179
|
-
let tidRegexp = /
|
|
1308
|
+
let tidRegexp = /Writer\/TID:(.+)\/TotalCount/;
|
|
1180
1309
|
if (bCurrentCount) tidRegexp = /Writer\/TID:(.+)\/CurrentCount/;
|
|
1181
1310
|
if (bExcludedCount) tidRegexp = /Writer\/TID:(.+)\/ExtCount/;
|
|
1182
1311
|
if (bDeferredCount) tidRegexp = /Writer\/TID:(.+)\/DeferredCount/;
|
|
@@ -1198,7 +1327,7 @@ export class BatchArray {
|
|
|
1198
1327
|
writerNode.total = Math.max(Number(tenantNode.total), writerNode.total);
|
|
1199
1328
|
writerNode.batchId = matchingPair.BatchId;
|
|
1200
1329
|
if (bTotalCount) {
|
|
1201
|
-
writerNode.total = Math.max(Number(
|
|
1330
|
+
writerNode.total = Math.max(Number(statsvalues[j]), writerNode.total);
|
|
1202
1331
|
console.log(`----- ${writerNode.name} TID: ${writerNode.tid} batchId: ${writerNode.batchId}`);
|
|
1203
1332
|
console.log(`----- ${writerNode.name} Total To Write: ${writerNode.total}`);
|
|
1204
1333
|
}
|
|
@@ -1232,6 +1361,7 @@ export class BatchArray {
|
|
|
1232
1361
|
let writerTotal: number = 0;
|
|
1233
1362
|
let writerCurrent: number = 0;
|
|
1234
1363
|
let writerExcluded: number = 0;
|
|
1364
|
+
let writerDeferred: number = 0;
|
|
1235
1365
|
this.tenantNodes.map((sourceTenantNode: TenantNode) => {
|
|
1236
1366
|
sourceTenantNode.targets.map((writerNode: TenantNode) => {
|
|
1237
1367
|
bWritingComplete &&= (writerNode.status == "complete" || writerNode.status == "failed");
|
|
@@ -1239,13 +1369,40 @@ export class BatchArray {
|
|
|
1239
1369
|
writerTotal += Math.max(writerNode.total, sourceTenantNode.total);
|
|
1240
1370
|
writerCurrent += writerNode.written;
|
|
1241
1371
|
writerExcluded += writerNode.excluded;
|
|
1372
|
+
writerDeferred += writerNode.deferred;
|
|
1242
1373
|
});
|
|
1243
1374
|
bNothingToSync &&= sourceTenantNode.nothingtosync;
|
|
1244
|
-
|
|
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);
|
|
1245
1377
|
readerTotal += sourceTenantNode.total;
|
|
1246
1378
|
readerCurrent += sourceTenantNode.read;
|
|
1247
1379
|
readerExcluded += sourceTenantNode.excluded;
|
|
1248
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
|
+
}
|
|
1249
1406
|
// set linear gauge max and current values
|
|
1250
1407
|
setReadersTotal(readerTotal);
|
|
1251
1408
|
setReadersCurrent(readerCurrent);
|
|
@@ -1253,11 +1410,34 @@ export class BatchArray {
|
|
|
1253
1410
|
setWritersTotal(Math.max(writerTotal, readerTotal));
|
|
1254
1411
|
setWritersCurrent(writerCurrent);
|
|
1255
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
|
+
);
|
|
1256
1427
|
// check to see if there was nothing to sync
|
|
1257
1428
|
if (bNothingToSync) {
|
|
1258
1429
|
this.milestoneArray.write(setMilestones);
|
|
1259
|
-
|
|
1430
|
+
this.stopPolling();
|
|
1260
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).`);
|
|
1440
|
+
this.clearStoredBatchIds();
|
|
1261
1441
|
console.log(`Setting config sync result: "nothing to sync"`);
|
|
1262
1442
|
}
|
|
1263
1443
|
else {
|
|
@@ -1268,19 +1448,22 @@ export class BatchArray {
|
|
|
1268
1448
|
console.log(`Setting config sync result: "reading complete"`);
|
|
1269
1449
|
// trigger refresh delta tokens
|
|
1270
1450
|
setRefreshDeltaTrigger(config!.workspaceId);
|
|
1271
|
-
// change to % per second to complete in 12x as long as it took to get here
|
|
1272
|
-
let readTS = Date.now();
|
|
1273
|
-
let secsElapsed = (readTS - this.pb_startTS) / 1000;
|
|
1274
|
-
let expectedPercentDone = 8.5;
|
|
1275
|
-
let expectedPercentPerSecond = secsElapsed / expectedPercentDone;
|
|
1276
|
-
this.pb_increment = expectedPercentPerSecond;
|
|
1277
|
-
console.log(`Setting increment: ${this.pb_increment}% per second`);
|
|
1278
1451
|
}
|
|
1279
1452
|
// with that out of the way, is writing complete?
|
|
1280
|
-
if
|
|
1453
|
+
// Only allow terminal completion when backend queues are empty (if we have queue info).
|
|
1454
|
+
if (bWritingComplete && (queuesEmpty || !this.hasQueueInfo)) {
|
|
1281
1455
|
this.milestoneArray.write(setMilestones);
|
|
1282
|
-
|
|
1456
|
+
this.stopPolling();
|
|
1283
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.`);
|
|
1466
|
+
this.clearStoredBatchIds();
|
|
1284
1467
|
console.log(`Setting config sync result: "complete"`);
|
|
1285
1468
|
}
|
|
1286
1469
|
// if not, has writing even started?
|
|
@@ -1296,38 +1479,26 @@ export class BatchArray {
|
|
|
1296
1479
|
}
|
|
1297
1480
|
}
|
|
1298
1481
|
|
|
1299
|
-
//
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
connection.onreconnected(connectionId => {
|
|
1320
|
-
console.assert(connection.state === signalR.HubConnectionState.Connected);
|
|
1321
|
-
console.log(`Connection reestablished. Connected with connectionId "${connectionId}".`);
|
|
1322
|
-
});
|
|
1323
|
-
// restart when you get a close event
|
|
1324
|
-
connection.onclose(async () => {
|
|
1325
|
-
console.log(`Connection closing. Attempting restart.`);
|
|
1326
|
-
await connection.start();
|
|
1327
|
-
});
|
|
1328
|
-
// start and display any caught exceptions in the console
|
|
1329
|
-
connection.start().catch(console.error);
|
|
1330
|
-
});
|
|
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
|
+
}
|
|
1331
1502
|
}
|
|
1332
1503
|
// start a sync cycle
|
|
1333
1504
|
async startSync(instance: IPublicClientApplication, authorizedUser: User | null | undefined, config: SyncConfig | null | undefined): Promise<APIResult> {
|
|
@@ -1375,8 +1546,20 @@ export class TenantNode {
|
|
|
1375
1546
|
this.deferred = deferred;
|
|
1376
1547
|
if (this.read === 0 && this.written === 0) this.status = "not started";
|
|
1377
1548
|
if (this.read > 0) {
|
|
1378
|
-
if (this.read + this.excluded < this.total)
|
|
1379
|
-
|
|
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
|
+
}
|
|
1380
1563
|
}
|
|
1381
1564
|
else if (this.written > 0) {
|
|
1382
1565
|
if (this.written + this.deferred + this.excluded < this.total) this.status = "in progress";
|
|
@@ -2187,7 +2370,9 @@ export async function configsRefresh(instance: IPublicClientApplication, authori
|
|
|
2187
2370
|
// console.log("Init Info-----------", currentConfig.id)
|
|
2188
2371
|
// tag components with workspaceIDs
|
|
2189
2372
|
ii.tagWithWorkspaces();
|
|
2190
|
-
|
|
2373
|
+
// IMPORTANT:
|
|
2374
|
+
// Do not clear BatchIdArray here. It is used by the UI to restore an in-flight sync
|
|
2375
|
+
// after refresh/re-login. BatchIdArray should be cleared when the sync completes.
|
|
2191
2376
|
return result;
|
|
2192
2377
|
} else {
|
|
2193
2378
|
// workspace not found
|
|
@@ -4176,4 +4361,4 @@ export async function readerStats(
|
|
|
4176
4361
|
console.log(error.message);
|
|
4177
4362
|
}
|
|
4178
4363
|
return result;
|
|
4179
|
-
}
|
|
4364
|
+
}
|
|
Binary file
|