@indigoai-us/hq-cloud 6.18.89 → 6.18.90
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/bin/sync-runner-self-profile.d.ts +225 -0
- package/dist/bin/sync-runner-self-profile.d.ts.map +1 -0
- package/dist/bin/sync-runner-self-profile.js +751 -0
- package/dist/bin/sync-runner-self-profile.js.map +1 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +96 -6
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +17 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +7 -0
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/watcher-catchup-skip-store.d.ts.map +1 -1
- package/dist/bin/watcher-catchup-skip-store.js +6 -0
- package/dist/bin/watcher-catchup-skip-store.js.map +1 -1
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +9 -0
- package/dist/ignore.js.map +1 -1
- package/dist/telemetry-events.d.ts +5 -0
- package/dist/telemetry-events.d.ts.map +1 -1
- package/dist/telemetry-events.js +9 -0
- package/dist/telemetry-events.js.map +1 -1
- package/dist/watcher.d.ts +50 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +195 -43
- package/dist/watcher.js.map +1 -1
- package/package.json +1 -1
package/dist/watcher.js
CHANGED
|
@@ -292,6 +292,9 @@ export class ChokidarWatchBudget {
|
|
|
292
292
|
this.admitted.add(resolved);
|
|
293
293
|
return true;
|
|
294
294
|
}
|
|
295
|
+
paths() {
|
|
296
|
+
return [...this.admitted];
|
|
297
|
+
}
|
|
295
298
|
count() {
|
|
296
299
|
return this.admitted.size;
|
|
297
300
|
}
|
|
@@ -422,6 +425,7 @@ function startChokidarTreeWatch(hqRoot, shouldEmit, onEvent, onError, maxWatched
|
|
|
422
425
|
mode: forcePolling ? "polling" : "chokidar",
|
|
423
426
|
needsKnownKinds: false,
|
|
424
427
|
watchedPathCount: () => budget.count(),
|
|
428
|
+
watchedDirectories: () => budget.paths().map((watched) => ({ path: watched, recursive: false })),
|
|
425
429
|
probeCoverage,
|
|
426
430
|
};
|
|
427
431
|
}
|
|
@@ -444,6 +448,7 @@ function startChokidarTreeWatch(hqRoot, shouldEmit, onEvent, onError, maxWatched
|
|
|
444
448
|
mode: forcePolling ? "polling" : "chokidar",
|
|
445
449
|
needsKnownKinds: false,
|
|
446
450
|
watchedPathCount: () => budget.count(),
|
|
451
|
+
watchedDirectories: () => budget.paths().map((watched) => ({ path: watched, recursive: false })),
|
|
447
452
|
probeCoverage,
|
|
448
453
|
onReady: (listener) => {
|
|
449
454
|
if (ready) {
|
|
@@ -484,6 +489,16 @@ function startChokidarTreeWatch(hqRoot, shouldEmit, onEvent, onError, maxWatched
|
|
|
484
489
|
* still drain instead of being dropped with the handle.
|
|
485
490
|
*/
|
|
486
491
|
const BOOTSTRAP_DRAIN_MS = 2000;
|
|
492
|
+
/**
|
|
493
|
+
* How many directory levels below the HQ root the planning bootstrap splits
|
|
494
|
+
* around excluded directories (worktrees, node_modules, nested .git, and
|
|
495
|
+
* buckets the filter rejects) before it falls back to one recursive watch.
|
|
496
|
+
* This covers the known worktree roots and the top of each company's project
|
|
497
|
+
* folders. An excluded directory deeper than this can sit under a bootstrap
|
|
498
|
+
* watch for the few seconds planning takes; the emit filter drops its events
|
|
499
|
+
* and the scoped plan that replaces the bootstrap never covers it.
|
|
500
|
+
*/
|
|
501
|
+
const BOOTSTRAP_SPLIT_DEPTH = 6;
|
|
487
502
|
/**
|
|
488
503
|
* How long after the recursive watch set changes (a re-plan, or a new
|
|
489
504
|
* directory attached) the backend waits before sweeping for files it may have
|
|
@@ -973,27 +988,35 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
|
|
|
973
988
|
sweepAfterSettle(rootDir, startedAt - WATCH_SET_SWEEP_LOOKBACK_MS);
|
|
974
989
|
});
|
|
975
990
|
}
|
|
976
|
-
// Cover the
|
|
977
|
-
//
|
|
991
|
+
// Cover the tree BEFORE planning. The plan walk below is synchronous and
|
|
992
|
+
// takes seconds on a real HQ root; without this, every change during that
|
|
978
993
|
// window would be invisible to event-driven push and would wait for the next
|
|
979
|
-
// cadence poll.
|
|
980
|
-
//
|
|
981
|
-
//
|
|
994
|
+
// cadence poll. These temporary watches are dropped once the scoped plan is
|
|
995
|
+
// live.
|
|
996
|
+
// They used to be one recursive watch on the root. That made the OS report
|
|
997
|
+
// every worktree checkout on the box while planning ran (on one Windows host,
|
|
998
|
+
// 157k files under a top-level `worktrees/`), so the bootstrap now splits
|
|
999
|
+
// around excluded directories the same way the plan does, only shallower: a
|
|
1000
|
+
// directory with no excluded child gets one recursive watch, otherwise it
|
|
1001
|
+
// gets a shallow watch and its in-scope children are split in turn, down to
|
|
1002
|
+
// BOOTSTRAP_SPLIT_DEPTH. Below that depth one recursive watch is taken and
|
|
1003
|
+
// the emit filter drops the excluded events, as before.
|
|
982
1004
|
// No such watch exists in shallow mode: Node's Linux "recursive" shim would
|
|
983
1005
|
// walk the WHOLE tree — worktrees, node_modules and all — to emulate it.
|
|
984
1006
|
// In shallow mode, each parent is attached before its child list is read.
|
|
985
|
-
|
|
986
|
-
|
|
1007
|
+
const bootstrap = [];
|
|
1008
|
+
const openBootstrap = (dir, recursive) => {
|
|
987
1009
|
try {
|
|
988
|
-
|
|
1010
|
+
const handle = (opts.watchDirectory ?? fs.watch)(dir, { recursive, persistent: true }, (eventType, filename) => {
|
|
989
1011
|
if (filename == null)
|
|
990
1012
|
return;
|
|
991
1013
|
const rel = String(filename);
|
|
992
1014
|
if (rel === "")
|
|
993
1015
|
return;
|
|
994
|
-
onEvent(path.resolve(
|
|
1016
|
+
onEvent(path.resolve(dir, rel), eventType === "change" ? "change" : "rename");
|
|
995
1017
|
});
|
|
996
|
-
|
|
1018
|
+
handle.on("error", onError);
|
|
1019
|
+
bootstrap.push({ handle, dir, recursive });
|
|
997
1020
|
}
|
|
998
1021
|
catch (err) {
|
|
999
1022
|
// Recursive watch is unavailable at all — the plan below cannot work
|
|
@@ -1001,7 +1024,64 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
|
|
|
1001
1024
|
attachFailures += 1;
|
|
1002
1025
|
onError(err);
|
|
1003
1026
|
}
|
|
1004
|
-
}
|
|
1027
|
+
};
|
|
1028
|
+
// Each directory is listed once even though the split asks about it from
|
|
1029
|
+
// every ancestor level.
|
|
1030
|
+
const bootstrapListings = new Map();
|
|
1031
|
+
const listBootstrapChildren = (dir) => {
|
|
1032
|
+
const cached = bootstrapListings.get(dir);
|
|
1033
|
+
if (cached)
|
|
1034
|
+
return cached;
|
|
1035
|
+
let children;
|
|
1036
|
+
try {
|
|
1037
|
+
children = opts.listChildDirs
|
|
1038
|
+
? opts.listChildDirs(dir)
|
|
1039
|
+
: fs
|
|
1040
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
1041
|
+
.filter((entry) => entry.isDirectory())
|
|
1042
|
+
.map((entry) => path.join(dir, entry.name));
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
children = [];
|
|
1046
|
+
}
|
|
1047
|
+
bootstrapListings.set(dir, children);
|
|
1048
|
+
return children;
|
|
1049
|
+
};
|
|
1050
|
+
// Only the heavy directories (worktrees, node_modules, nested .git) are split
|
|
1051
|
+
// around. The emit filter is not consulted here: no part of the tree under
|
|
1052
|
+
// the split is watched until the split finishes, so it has to stay as short
|
|
1053
|
+
// as a directory listing. Filter-excluded directories are covered for the
|
|
1054
|
+
// planning window and their events dropped by the filter, as before.
|
|
1055
|
+
const bootstrapExcluded = (dir) => heavyWatchKind(path.relative(root, dir)) !== null;
|
|
1056
|
+
/** True when an excluded directory sits within `levels` levels below `dir`. */
|
|
1057
|
+
const hasExcludedWithin = (dir, levels) => {
|
|
1058
|
+
if (levels <= 0)
|
|
1059
|
+
return false;
|
|
1060
|
+
for (const child of listBootstrapChildren(dir)) {
|
|
1061
|
+
if (bootstrapExcluded(child) || hasExcludedWithin(child, levels - 1)) {
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return false;
|
|
1066
|
+
};
|
|
1067
|
+
const coverForBootstrap = (dir, depth) => {
|
|
1068
|
+
if (depth >= BOOTSTRAP_SPLIT_DEPTH) {
|
|
1069
|
+
openBootstrap(dir, true);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (dir !== root && !hasExcludedWithin(dir, BOOTSTRAP_SPLIT_DEPTH - depth)) {
|
|
1073
|
+
openBootstrap(dir, true);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
openBootstrap(dir, false);
|
|
1077
|
+
for (const child of listBootstrapChildren(dir)) {
|
|
1078
|
+
if (!bootstrapExcluded(child))
|
|
1079
|
+
coverForBootstrap(child, depth + 1);
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
if (recursiveWatches)
|
|
1083
|
+
coverForBootstrap(root, 0);
|
|
1084
|
+
bootstrapListings.clear();
|
|
1005
1085
|
if (!recursiveWatches) {
|
|
1006
1086
|
addWatch(root, false);
|
|
1007
1087
|
void planShallowAndAttach(root, true);
|
|
@@ -1020,29 +1100,20 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
|
|
|
1020
1100
|
// them, reopening the very gap the bootstrap watch exists to close. Hold it
|
|
1021
1101
|
// open for one drain window instead; the overlap only costs duplicate events
|
|
1022
1102
|
// for excluded paths, which the emit filter drops and the debounce coalesces.
|
|
1023
|
-
if (bootstrap) {
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
}
|
|
1030
|
-
catch {
|
|
1031
|
-
/* already closed */
|
|
1032
|
-
}
|
|
1103
|
+
if (bootstrap.length > 0) {
|
|
1104
|
+
const handles = bootstrap.map(({ handle }) => handle);
|
|
1105
|
+
const closeAll = () => {
|
|
1106
|
+
for (const handle of handles)
|
|
1107
|
+
closeHandle(handle);
|
|
1108
|
+
bootstrap.length = 0;
|
|
1033
1109
|
closeBootstrap = () => { };
|
|
1034
|
-
}
|
|
1110
|
+
};
|
|
1111
|
+
const timer = setTimeout(closeAll, BOOTSTRAP_DRAIN_MS);
|
|
1035
1112
|
// Never hold the process open just to retire a temporary watch.
|
|
1036
1113
|
timer.unref?.();
|
|
1037
1114
|
closeBootstrap = () => {
|
|
1038
1115
|
clearTimeout(timer);
|
|
1039
|
-
|
|
1040
|
-
handle.close();
|
|
1041
|
-
}
|
|
1042
|
-
catch {
|
|
1043
|
-
/* already closed */
|
|
1044
|
-
}
|
|
1045
|
-
closeBootstrap = () => { };
|
|
1116
|
+
closeAll();
|
|
1046
1117
|
};
|
|
1047
1118
|
}
|
|
1048
1119
|
}
|
|
@@ -1081,6 +1152,13 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
|
|
|
1081
1152
|
return () => readyListeners.delete(listener);
|
|
1082
1153
|
},
|
|
1083
1154
|
watchedPathCount: () => watchers.size,
|
|
1155
|
+
watchedDirectories: () => [
|
|
1156
|
+
...bootstrap.map(({ dir, recursive }) => ({ path: dir, recursive })),
|
|
1157
|
+
...[...watchers.keys()].map((dir) => ({
|
|
1158
|
+
path: dir,
|
|
1159
|
+
recursive: recursiveFlag.get(dir) === true,
|
|
1160
|
+
})),
|
|
1161
|
+
],
|
|
1084
1162
|
attachFailures,
|
|
1085
1163
|
probeCoverage: () => probeScopedWatchCoverage({
|
|
1086
1164
|
root,
|
|
@@ -1168,6 +1246,7 @@ export function startNativeRootWatch(hqRoot, shouldEmit, onEvent, onError, opts
|
|
|
1168
1246
|
needsKnownKinds: true,
|
|
1169
1247
|
attachFailures,
|
|
1170
1248
|
watchedPathCount: () => (handle ? 1 : 0),
|
|
1249
|
+
watchedDirectories: () => (handle ? [{ path: root, recursive: true }] : []),
|
|
1171
1250
|
probeCoverage: () => probeScopedWatchCoverage({
|
|
1172
1251
|
root,
|
|
1173
1252
|
identities: identity,
|
|
@@ -1248,21 +1327,42 @@ export function startTreeWatch(hqRoot, shouldEmit, onEvent, onError, maxWatchedP
|
|
|
1248
1327
|
return startChokidarTreeWatch(root, watchFilter, watchEvent, onError, maxWatchedPaths, onWatchBudgetExceeded, forcePolling);
|
|
1249
1328
|
}
|
|
1250
1329
|
/**
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
1253
|
-
*
|
|
1254
|
-
*
|
|
1330
|
+
* Classify a root-relative path that the watcher must never watch, at any
|
|
1331
|
+
* depth: git worktree roots (`worktrees/` at the top level, `.worktrees/`,
|
|
1332
|
+
* `workspace/worktrees/` and `.claude/worktrees/` anywhere), `node_modules/`,
|
|
1333
|
+
* and a `.git` below the root. Returns the kind for the path itself or for an
|
|
1334
|
+
* ancestor, or null when the path is not inside one.
|
|
1255
1335
|
*
|
|
1256
|
-
*
|
|
1257
|
-
*
|
|
1258
|
-
*
|
|
1259
|
-
* @param options.includeCompanyPaths retains `companies/<slug>/` below that
|
|
1260
|
-
* otherwise-excluded top-level bucket. The combined
|
|
1261
|
-
* `--companies` runner needs this: it syncs company
|
|
1262
|
-
* vaults and the personal vault in one process, while the
|
|
1263
|
-
* personal side must still prune unrelated `workspace/`
|
|
1264
|
-
* churn.
|
|
1336
|
+
* A plain `worktrees` folder below the top level is not matched, so a
|
|
1337
|
+
* knowledge folder with that name keeps live watching (the DEV-1791 rule the
|
|
1338
|
+
* sync ignore list follows for the same reason).
|
|
1265
1339
|
*/
|
|
1340
|
+
export function heavyWatchKind(relativePath) {
|
|
1341
|
+
// Compared lower-cased: Windows and macOS file systems are case-insensitive,
|
|
1342
|
+
// so `Worktrees`, `.Worktrees` and `Node_Modules` are the same folders.
|
|
1343
|
+
const segments = relativePath
|
|
1344
|
+
.split(/[\\/]+/)
|
|
1345
|
+
.filter((segment) => segment !== "")
|
|
1346
|
+
.map((segment) => segment.toLowerCase());
|
|
1347
|
+
if (segments[0] === "worktrees")
|
|
1348
|
+
return "worktrees";
|
|
1349
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
1350
|
+
const segment = segments[i];
|
|
1351
|
+
if (segment === "node_modules")
|
|
1352
|
+
return "node_modules";
|
|
1353
|
+
if (segment === ".worktrees")
|
|
1354
|
+
return "worktrees";
|
|
1355
|
+
if (segment === "worktrees" &&
|
|
1356
|
+
(segments[i - 1] === "workspace" || segments[i - 1] === ".claude")) {
|
|
1357
|
+
return "worktrees";
|
|
1358
|
+
}
|
|
1359
|
+
// The HQ root's own .git is excluded by the ignore stack; one below the
|
|
1360
|
+
// root is a nested checkout.
|
|
1361
|
+
if (segment === ".git" && i > 0)
|
|
1362
|
+
return "git";
|
|
1363
|
+
}
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1266
1366
|
export function createWatchPathFilter(hqRoot, personalMode = false, options = {}) {
|
|
1267
1367
|
const ignoreFilter = createIgnoreFilter(hqRoot);
|
|
1268
1368
|
const personalScope = personalMode
|
|
@@ -1290,6 +1390,15 @@ export function createWatchPathFilter(hqRoot, personalMode = false, options = {}
|
|
|
1290
1390
|
// The root itself, or anything outside it, is never an emit target.
|
|
1291
1391
|
if (rel === "" || rel.startsWith(".."))
|
|
1292
1392
|
return false;
|
|
1393
|
+
// Worktrees, node_modules and nested .git are never watched, at any depth
|
|
1394
|
+
// and in every mode. Checked before the ignore stack and every carve-out
|
|
1395
|
+
// so no .hqinclude or workspace carve-out can re-admit them.
|
|
1396
|
+
// A file probe checks only its folders: `node_modules/` is directory-only
|
|
1397
|
+
// in the ignore stack, and chokidar's pre-stat probe is file-shaped for a
|
|
1398
|
+
// new directory, so the stat-aware directory verdict decides that case.
|
|
1399
|
+
const watchDirs = isDir ? rel : rel.slice(0, Math.max(0, rel.lastIndexOf("/")));
|
|
1400
|
+
if (heavyWatchKind(watchDirs) !== null)
|
|
1401
|
+
return false;
|
|
1293
1402
|
// Layer 1: shared ignore stack (.hqignore/.gitignore/DEFAULT_IGNORES).
|
|
1294
1403
|
// createIgnoreFilter returns true = "sync this"; false = "ignored".
|
|
1295
1404
|
if (!ignoreFilter(absolutePath, isDir))
|
|
@@ -1511,6 +1620,9 @@ export class TreeWatcher {
|
|
|
1511
1620
|
smallBatchMaxPaths;
|
|
1512
1621
|
smallBatchMaxBytes;
|
|
1513
1622
|
maxWaitMs;
|
|
1623
|
+
minEmitGapMs;
|
|
1624
|
+
/** clock.now() of the last batch handed to listeners; null before the first. */
|
|
1625
|
+
lastEmitAt = null;
|
|
1514
1626
|
clock;
|
|
1515
1627
|
shouldEmit;
|
|
1516
1628
|
maxPendingPaths;
|
|
@@ -1591,6 +1703,10 @@ export class TreeWatcher {
|
|
|
1591
1703
|
opts.maxWaitMs !== undefined && opts.maxWaitMs > 0
|
|
1592
1704
|
? Math.max(opts.maxWaitMs, this.debounceMs)
|
|
1593
1705
|
: Infinity;
|
|
1706
|
+
this.minEmitGapMs =
|
|
1707
|
+
opts.minEmitGapMs !== undefined && Number.isFinite(opts.minEmitGapMs) && opts.minEmitGapMs > 0
|
|
1708
|
+
? Math.floor(opts.minEmitGapMs)
|
|
1709
|
+
: 0;
|
|
1594
1710
|
this.clock = opts.clock ?? systemClock;
|
|
1595
1711
|
this.shouldEmit =
|
|
1596
1712
|
opts.pathFilter ?? createWatchPathFilter(opts.hqRoot, opts.personalMode ?? false, {
|
|
@@ -2322,9 +2438,15 @@ export class TreeWatcher {
|
|
|
2322
2438
|
: this.debounceMs;
|
|
2323
2439
|
// Quiet-period debounce, capped by the window's max-wait deadline so a
|
|
2324
2440
|
// continuous event stream cannot postpone the emit forever.
|
|
2325
|
-
const
|
|
2441
|
+
const settleMs = this.windowDeadlineAt === null
|
|
2326
2442
|
? quietWindowMs
|
|
2327
2443
|
: Math.max(0, Math.min(quietWindowMs, this.windowDeadlineAt - now));
|
|
2444
|
+
// The pass-gap cap wins over both: a batch never emits sooner than
|
|
2445
|
+
// minEmitGapMs after the previous one.
|
|
2446
|
+
const gapMs = this.lastEmitAt === null || this.minEmitGapMs === 0
|
|
2447
|
+
? 0
|
|
2448
|
+
: Math.max(0, this.lastEmitAt + this.minEmitGapMs - now);
|
|
2449
|
+
const delayMs = Math.max(settleMs, gapMs);
|
|
2328
2450
|
this.timer = this.clock.setTimeout(() => {
|
|
2329
2451
|
this.timer = null;
|
|
2330
2452
|
this.emit();
|
|
@@ -2360,6 +2482,7 @@ export class TreeWatcher {
|
|
|
2360
2482
|
// undefined when the window settled with no captured path (e.g. a synthetic
|
|
2361
2483
|
// arm() with no handleEvent).
|
|
2362
2484
|
const firstRel = [...batch.paths.values()][0];
|
|
2485
|
+
this.lastEmitAt = this.clock.now();
|
|
2363
2486
|
for (const l of this.listeners) {
|
|
2364
2487
|
try {
|
|
2365
2488
|
l(firstRel, batch);
|
|
@@ -2757,6 +2880,16 @@ export class TreeWatcher {
|
|
|
2757
2880
|
watchedPathCount() {
|
|
2758
2881
|
return this.backend?.watchedPathCount() ?? 0;
|
|
2759
2882
|
}
|
|
2883
|
+
/** OS-level watches held by the active backend, for diagnostics. */
|
|
2884
|
+
watchedDirectories() {
|
|
2885
|
+
return this.backend?.watchedDirectories?.() ?? [];
|
|
2886
|
+
}
|
|
2887
|
+
/** Backend mode before folding native-root into native. */
|
|
2888
|
+
watchBackend() {
|
|
2889
|
+
if (!this.backend)
|
|
2890
|
+
return "inactive";
|
|
2891
|
+
return this.backend.mode ?? "unknown";
|
|
2892
|
+
}
|
|
2760
2893
|
/** Active backend mode, including the no-inotify polling fallback. */
|
|
2761
2894
|
watchMode() {
|
|
2762
2895
|
const mode = this.backend?.mode;
|
|
@@ -2869,6 +3002,25 @@ export const DEFAULT_PUBLISHED_CONTENT_HASH_MAX_PATHS = 50_000;
|
|
|
2869
3002
|
/** Minimum delay between successful upsert announcements for one path. */
|
|
2870
3003
|
export const DEFAULT_PUBLISH_MIN_INTERVAL_MS = 60_000;
|
|
2871
3004
|
export const PUBLISH_MIN_INTERVAL_MS_ENV = "HQ_SYNC_PUBLISH_MIN_INTERVAL_MS";
|
|
3005
|
+
/**
|
|
3006
|
+
* Minimum time between two watcher batches, and so between two
|
|
3007
|
+
* watch-triggered passes. Events that arrive inside the gap are held and
|
|
3008
|
+
* merged into the next batch, which emits once the gap has passed. This caps
|
|
3009
|
+
* the pass rate even when the debounce windows are tuned short or a stream of
|
|
3010
|
+
* small batches keeps settling. `HQ_SYNC_MIN_PASS_GAP_MS` overrides it; `0`
|
|
3011
|
+
* turns the cap off, and an invalid value falls back to the default.
|
|
3012
|
+
*/
|
|
3013
|
+
export const DEFAULT_MIN_PASS_GAP_MS = 30_000;
|
|
3014
|
+
export const MIN_PASS_GAP_MS_ENV = "HQ_SYNC_MIN_PASS_GAP_MS";
|
|
3015
|
+
export function resolveMinPassGapMs(env = process.env) {
|
|
3016
|
+
const raw = env[MIN_PASS_GAP_MS_ENV];
|
|
3017
|
+
if (raw === undefined || raw.trim() === "")
|
|
3018
|
+
return DEFAULT_MIN_PASS_GAP_MS;
|
|
3019
|
+
const value = Number(raw);
|
|
3020
|
+
return Number.isFinite(value) && value >= 0
|
|
3021
|
+
? Math.floor(value)
|
|
3022
|
+
: DEFAULT_MIN_PASS_GAP_MS;
|
|
3023
|
+
}
|
|
2872
3024
|
/** Resolve the optional per-path announcement interval without accepting invalid values. */
|
|
2873
3025
|
export function resolvePublishMinIntervalMs(env = process.env) {
|
|
2874
3026
|
const raw = env[PUBLISH_MIN_INTERVAL_MS_ENV];
|