@indigoai-us/hq-cloud 6.18.33 → 6.18.35
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-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +31 -8
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +4 -2
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +3 -1
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/reindex-async.d.ts +9 -0
- package/dist/cli/reindex-async.d.ts.map +1 -0
- package/dist/cli/reindex-async.js +76 -0
- package/dist/cli/reindex-async.js.map +1 -0
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +1 -1
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/rescue-rsync-preflight.test.js +1 -1
- package/dist/cli/rescue-rsync-preflight.test.js.map +1 -1
- package/dist/journal.d.ts +1 -0
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +1 -1
- package/dist/journal.js.map +1 -1
- package/dist/sync/full-pass-cap-governor.bench.test.d.ts +2 -0
- package/dist/sync/full-pass-cap-governor.bench.test.d.ts.map +1 -0
- package/dist/sync/full-pass-cap-governor.bench.test.js +179 -0
- package/dist/sync/full-pass-cap-governor.bench.test.js.map +1 -0
- package/dist/sync/full-pass-caps.d.ts +12 -2
- package/dist/sync/full-pass-caps.d.ts.map +1 -1
- package/dist/sync/full-pass-caps.js +78 -38
- package/dist/sync/full-pass-caps.js.map +1 -1
- package/dist/sync/full-pass-caps.test.js +144 -1
- package/dist/sync/full-pass-caps.test.js.map +1 -1
- package/dist/watcher-replan-sweep.test.js +21 -3
- package/dist/watcher-replan-sweep.test.js.map +1 -1
- package/dist/watcher.d.ts +2 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +166 -50
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +59 -5
- package/dist/watcher.test.js.map +1 -1
- package/dist/watcher.warmup-cancel.test.d.ts +2 -0
- package/dist/watcher.warmup-cancel.test.d.ts.map +1 -0
- package/dist/watcher.warmup-cancel.test.js +59 -0
- package/dist/watcher.warmup-cancel.test.js.map +1 -0
- package/dist/watcher.warmup-responsive.test.d.ts +2 -0
- package/dist/watcher.warmup-responsive.test.d.ts.map +1 -0
- package/dist/watcher.warmup-responsive.test.js +62 -0
- package/dist/watcher.warmup-responsive.test.js.map +1 -0
- package/package.json +3 -2
package/dist/watcher.js
CHANGED
|
@@ -528,6 +528,16 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
|
|
|
528
528
|
let budgetExhausted = false;
|
|
529
529
|
/** True once the initial plan is live; later attaches announce their files. */
|
|
530
530
|
let planned = false;
|
|
531
|
+
let initialDiscoveryComplete = false;
|
|
532
|
+
const readyListeners = new Set();
|
|
533
|
+
const notifyReady = () => {
|
|
534
|
+
if (initialDiscoveryComplete || closed)
|
|
535
|
+
return;
|
|
536
|
+
initialDiscoveryComplete = true;
|
|
537
|
+
for (const listener of readyListeners)
|
|
538
|
+
listener();
|
|
539
|
+
readyListeners.clear();
|
|
540
|
+
};
|
|
531
541
|
/** Retires the bootstrap watch — replaced once that watch exists. */
|
|
532
542
|
let closeBootstrap = () => { };
|
|
533
543
|
/**
|
|
@@ -586,6 +596,8 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
|
|
|
586
596
|
}
|
|
587
597
|
/** `dev:ino` of the directory at `absolutePath`; undefined if absent or not a directory. */
|
|
588
598
|
function directoryIdentity(absolutePath) {
|
|
599
|
+
if (opts.directoryIdentity)
|
|
600
|
+
return opts.directoryIdentity(absolutePath);
|
|
589
601
|
try {
|
|
590
602
|
const st = fs.lstatSync(absolutePath);
|
|
591
603
|
return st.isDirectory() ? `${st.dev}:${st.ino}` : undefined;
|
|
@@ -719,7 +731,7 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
|
|
|
719
731
|
addWatch(dir, false);
|
|
720
732
|
}
|
|
721
733
|
/**
|
|
722
|
-
* Plan `dir` and attach the result. In shallow mode every in-scope
|
|
734
|
+
* Plan `dir` and attach the result on recursive-watch platforms. In shallow mode every in-scope
|
|
723
735
|
* directory is attached the moment the walk discovers it — BEFORE the walk
|
|
724
736
|
* lists that directory — rather than after the whole plan is computed. The
|
|
725
737
|
* plan is a snapshot: a child created after its parent was listed is in
|
|
@@ -747,6 +759,66 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
|
|
|
747
759
|
listChildDirs: opts.listChildDirs,
|
|
748
760
|
}));
|
|
749
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* Linux needs one inotify watch per included directory. Keep the parent
|
|
764
|
+
* attached before listing it, but yield regularly so a large root cannot
|
|
765
|
+
* hold the runner's event loop while tens of thousands of watches open.
|
|
766
|
+
*/
|
|
767
|
+
async function planShallowAndAttach(dir, initial = false) {
|
|
768
|
+
const pending = [dir];
|
|
769
|
+
let workSinceYield = 0;
|
|
770
|
+
const yieldIfNeeded = async () => {
|
|
771
|
+
workSinceYield += 1;
|
|
772
|
+
if (workSinceYield < 128)
|
|
773
|
+
return closed;
|
|
774
|
+
workSinceYield = 0;
|
|
775
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
776
|
+
return closed;
|
|
777
|
+
};
|
|
778
|
+
while (pending.length > 0 && !closed) {
|
|
779
|
+
const current = pending.pop();
|
|
780
|
+
try {
|
|
781
|
+
if (opts.listChildDirs) {
|
|
782
|
+
for (const child of opts.listChildDirs(current)) {
|
|
783
|
+
if (shouldEmit(child, true)) {
|
|
784
|
+
onDirectorySeen?.(path.resolve(child));
|
|
785
|
+
addWatch(child, false);
|
|
786
|
+
pending.push(child);
|
|
787
|
+
}
|
|
788
|
+
if (await yieldIfNeeded())
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
const entries = await fs.promises.readdir(current, { withFileTypes: true });
|
|
794
|
+
for (const entry of entries) {
|
|
795
|
+
const absolutePath = path.join(current, entry.name);
|
|
796
|
+
if (entry.isDirectory()) {
|
|
797
|
+
if (shouldEmit(absolutePath, true)) {
|
|
798
|
+
onDirectorySeen?.(path.resolve(absolutePath));
|
|
799
|
+
addWatch(absolutePath, false);
|
|
800
|
+
pending.push(absolutePath);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
else if (!initial && shouldEmit(absolutePath, false)) {
|
|
804
|
+
onEvent(absolutePath, "rename");
|
|
805
|
+
}
|
|
806
|
+
if (await yieldIfNeeded())
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
const code = err?.code;
|
|
813
|
+
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
814
|
+
onError(err);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (initial) {
|
|
818
|
+
planned = true;
|
|
819
|
+
notifyReady();
|
|
820
|
+
}
|
|
821
|
+
}
|
|
750
822
|
/**
|
|
751
823
|
* Announce every in-scope file already inside a directory that was just
|
|
752
824
|
* attached. A shallow parent watch reports only that the directory
|
|
@@ -853,6 +925,11 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
|
|
|
853
925
|
return;
|
|
854
926
|
const attachedAt = Date.now();
|
|
855
927
|
onDirectorySeen?.(absolutePath);
|
|
928
|
+
if (!recursiveWatches) {
|
|
929
|
+
addWatch(absolutePath, false);
|
|
930
|
+
void planShallowAndAttach(absolutePath);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
856
933
|
planAndAttach(absolutePath);
|
|
857
934
|
if (!planned)
|
|
858
935
|
return;
|
|
@@ -903,7 +980,7 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
|
|
|
903
980
|
// dropped the moment the scoped plan is live.
|
|
904
981
|
// No such watch exists in shallow mode: Node's Linux "recursive" shim would
|
|
905
982
|
// walk the WHOLE tree — worktrees, node_modules and all — to emulate it.
|
|
906
|
-
//
|
|
983
|
+
// In shallow mode, each parent is attached before its child list is read.
|
|
907
984
|
let bootstrap = null;
|
|
908
985
|
if (recursiveWatches) {
|
|
909
986
|
try {
|
|
@@ -924,42 +1001,49 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
|
|
|
924
1001
|
onError(err);
|
|
925
1002
|
}
|
|
926
1003
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1004
|
+
if (!recursiveWatches) {
|
|
1005
|
+
addWatch(root, false);
|
|
1006
|
+
void planShallowAndAttach(root, true);
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
// Unbounded on purpose: this walk replaces TreeWatcher's separate known-kinds
|
|
1010
|
+
// seed walk (same tree, same filter, ~3.6s on a real HQ root), so planning
|
|
1011
|
+
// the tree exhaustively costs nothing over the status quo and leaves no
|
|
1012
|
+
// excluded subtree covered by a recursive root.
|
|
1013
|
+
planAndAttach(root);
|
|
1014
|
+
planned = true;
|
|
1015
|
+
notifyReady();
|
|
1016
|
+
// The scoped plan is live, so narrow from whole-tree to in-scope-only — but
|
|
1017
|
+
// NOT synchronously. Events observed during the planning walk can still be
|
|
1018
|
+
// sitting in the OS queue undelivered, and closing the handle here drops
|
|
1019
|
+
// them, reopening the very gap the bootstrap watch exists to close. Hold it
|
|
1020
|
+
// open for one drain window instead; the overlap only costs duplicate events
|
|
1021
|
+
// for excluded paths, which the emit filter drops and the debounce coalesces.
|
|
1022
|
+
if (bootstrap) {
|
|
1023
|
+
const handle = bootstrap;
|
|
1024
|
+
bootstrap = null;
|
|
1025
|
+
const timer = setTimeout(() => {
|
|
1026
|
+
try {
|
|
1027
|
+
handle.close();
|
|
1028
|
+
}
|
|
1029
|
+
catch {
|
|
1030
|
+
/* already closed */
|
|
1031
|
+
}
|
|
1032
|
+
closeBootstrap = () => { };
|
|
1033
|
+
}, BOOTSTRAP_DRAIN_MS);
|
|
1034
|
+
// Never hold the process open just to retire a temporary watch.
|
|
1035
|
+
timer.unref?.();
|
|
1036
|
+
closeBootstrap = () => {
|
|
1037
|
+
clearTimeout(timer);
|
|
1038
|
+
try {
|
|
1039
|
+
handle.close();
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
/* already closed */
|
|
1043
|
+
}
|
|
1044
|
+
closeBootstrap = () => { };
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
963
1047
|
}
|
|
964
1048
|
return {
|
|
965
1049
|
close: () => {
|
|
@@ -987,6 +1071,14 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
|
|
|
987
1071
|
mode: "native",
|
|
988
1072
|
needsKnownKinds: true,
|
|
989
1073
|
seededKnownKinds: onDirectorySeen !== undefined,
|
|
1074
|
+
onReady: (listener) => {
|
|
1075
|
+
if (initialDiscoveryComplete) {
|
|
1076
|
+
listener();
|
|
1077
|
+
return () => { };
|
|
1078
|
+
}
|
|
1079
|
+
readyListeners.add(listener);
|
|
1080
|
+
return () => readyListeners.delete(listener);
|
|
1081
|
+
},
|
|
990
1082
|
watchedPathCount: () => watchers.size,
|
|
991
1083
|
attachFailures,
|
|
992
1084
|
probeCoverage: () => probeScopedWatchCoverage({
|
|
@@ -1945,7 +2037,7 @@ export class TreeWatcher {
|
|
|
1945
2037
|
if (marker)
|
|
1946
2038
|
marker.epoch += 1;
|
|
1947
2039
|
}
|
|
1948
|
-
enqueueDeleteSnapshotCapture(absolutePath, relativePath, kind) {
|
|
2040
|
+
enqueueDeleteSnapshotCapture(absolutePath, relativePath, kind, deferBatchSchedule = false) {
|
|
1949
2041
|
// This delete supersedes whatever capture is already in flight for the
|
|
1950
2042
|
// path, whether or not the queue has room for a new one.
|
|
1951
2043
|
const live = this.deleteSnapshotEpochs.get(absolutePath);
|
|
@@ -1967,7 +2059,7 @@ export class TreeWatcher {
|
|
|
1967
2059
|
this.deleteSnapshotEpochs.set(absolutePath, marker);
|
|
1968
2060
|
const isCurrent = () => this.deleteSnapshotEpochs.get(absolutePath)?.epoch === epoch;
|
|
1969
2061
|
if (this.queueLocalDeleteSnapshotsBatch) {
|
|
1970
|
-
return this.enqueueDeleteSnapshotCaptureBatch(absolutePath, { relativePath, kind, isCurrent });
|
|
2062
|
+
return this.enqueueDeleteSnapshotCaptureBatch(absolutePath, { relativePath, kind, isCurrent }, !deferBatchSchedule);
|
|
1971
2063
|
}
|
|
1972
2064
|
const run = this.deleteSnapshotCaptureTail.then(() => this.queueLocalDeleteSnapshots(relativePath, kind, isCurrent), () => this.queueLocalDeleteSnapshots(relativePath, kind, isCurrent));
|
|
1973
2065
|
this.deleteSnapshotCaptureTail = run.then(() => undefined, () => undefined);
|
|
@@ -1975,7 +2067,7 @@ export class TreeWatcher {
|
|
|
1975
2067
|
void capture.catch(() => undefined);
|
|
1976
2068
|
return capture;
|
|
1977
2069
|
}
|
|
1978
|
-
enqueueDeleteSnapshotCaptureBatch(absolutePath, capture) {
|
|
2070
|
+
enqueueDeleteSnapshotCaptureBatch(absolutePath, capture, schedule = true) {
|
|
1979
2071
|
const promise = new Promise((resolve, reject) => {
|
|
1980
2072
|
this.pendingDeleteSnapshotCaptureBatch.push({
|
|
1981
2073
|
absolutePath,
|
|
@@ -1985,7 +2077,8 @@ export class TreeWatcher {
|
|
|
1985
2077
|
});
|
|
1986
2078
|
});
|
|
1987
2079
|
void promise.catch(() => undefined);
|
|
1988
|
-
|
|
2080
|
+
if (schedule)
|
|
2081
|
+
this.scheduleDeleteSnapshotCaptureBatch();
|
|
1989
2082
|
return promise;
|
|
1990
2083
|
}
|
|
1991
2084
|
scheduleDeleteSnapshotCaptureBatch() {
|
|
@@ -2345,22 +2438,29 @@ export class TreeWatcher {
|
|
|
2345
2438
|
cancelled: true,
|
|
2346
2439
|
});
|
|
2347
2440
|
const yieldToEventLoop = async () => {
|
|
2348
|
-
if (cancelled())
|
|
2441
|
+
if (cancelled()) {
|
|
2442
|
+
if (this.pendingDeleteSnapshotCaptureBatch.length > 0) {
|
|
2443
|
+
this.scheduleDeleteSnapshotCaptureBatch();
|
|
2444
|
+
}
|
|
2349
2445
|
return false;
|
|
2446
|
+
}
|
|
2350
2447
|
workSinceYield += 1;
|
|
2351
2448
|
if (workSinceYield < yieldEvery)
|
|
2352
2449
|
return true;
|
|
2353
2450
|
workSinceYield = 0;
|
|
2451
|
+
if (this.pendingDeleteSnapshotCaptureBatch.length > 0) {
|
|
2452
|
+
this.scheduleDeleteSnapshotCaptureBatch();
|
|
2453
|
+
}
|
|
2354
2454
|
await new Promise((resolve) => setImmediate(resolve));
|
|
2355
2455
|
return !cancelled();
|
|
2356
2456
|
};
|
|
2357
|
-
const record = (absolutePath, kind) => {
|
|
2457
|
+
const record = async (absolutePath, kind) => {
|
|
2358
2458
|
const absolute = path.resolve(absolutePath);
|
|
2359
2459
|
const relative = vaultKeyForLocalPath(this.hqRoot, absolute);
|
|
2360
2460
|
observed.add(relative);
|
|
2361
2461
|
let entry;
|
|
2362
2462
|
try {
|
|
2363
|
-
entry = fs.
|
|
2463
|
+
entry = await fs.promises.lstat(absolute);
|
|
2364
2464
|
}
|
|
2365
2465
|
catch {
|
|
2366
2466
|
return;
|
|
@@ -2376,7 +2476,7 @@ export class TreeWatcher {
|
|
|
2376
2476
|
return false;
|
|
2377
2477
|
let entries;
|
|
2378
2478
|
try {
|
|
2379
|
-
entries = fs.
|
|
2479
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
2380
2480
|
}
|
|
2381
2481
|
catch {
|
|
2382
2482
|
return !cancelled();
|
|
@@ -2396,7 +2496,7 @@ export class TreeWatcher {
|
|
|
2396
2496
|
if (entry.isFile() || entry.isSymbolicLink()) {
|
|
2397
2497
|
// A symlink may be admitted only by a directory-only include rule.
|
|
2398
2498
|
if (this.shouldEmit(absolute, false) || this.shouldEmit(absolute, true)) {
|
|
2399
|
-
record(absolute, "change");
|
|
2499
|
+
await record(absolute, "change");
|
|
2400
2500
|
}
|
|
2401
2501
|
}
|
|
2402
2502
|
if (!(await yieldToEventLoop()))
|
|
@@ -2421,8 +2521,17 @@ export class TreeWatcher {
|
|
|
2421
2521
|
return cancelledResult();
|
|
2422
2522
|
continue;
|
|
2423
2523
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2524
|
+
let present = false;
|
|
2525
|
+
try {
|
|
2526
|
+
await fs.promises.lstat(absolute);
|
|
2527
|
+
present = true;
|
|
2528
|
+
}
|
|
2529
|
+
catch {
|
|
2530
|
+
// Missing paths become deletes; other errors preserve the previous
|
|
2531
|
+
// behavior of treating an unreadable path as absent.
|
|
2532
|
+
}
|
|
2533
|
+
if (present) {
|
|
2534
|
+
await record(absolute, "change");
|
|
2426
2535
|
if (!(await yieldToEventLoop()))
|
|
2427
2536
|
return cancelledResult();
|
|
2428
2537
|
continue;
|
|
@@ -2432,7 +2541,7 @@ export class TreeWatcher {
|
|
|
2432
2541
|
let deleteSnapshotCapture;
|
|
2433
2542
|
try {
|
|
2434
2543
|
if (this.queueLocalDeleteSnapshotsBatch || this.queueLocalDeleteSnapshots) {
|
|
2435
|
-
deleteSnapshotCapture = this.enqueueDeleteSnapshotCapture(absolute, relative, "unlink");
|
|
2544
|
+
deleteSnapshotCapture = this.enqueueDeleteSnapshotCapture(absolute, relative, "unlink", true);
|
|
2436
2545
|
}
|
|
2437
2546
|
else {
|
|
2438
2547
|
deleteSnapshots = this.captureLocalDeleteSnapshots?.(relative, "unlink") ?? [];
|
|
@@ -2449,6 +2558,13 @@ export class TreeWatcher {
|
|
|
2449
2558
|
if (!(await yieldToEventLoop()))
|
|
2450
2559
|
return cancelledResult();
|
|
2451
2560
|
}
|
|
2561
|
+
// Keep the warm-up rows from the same bounded slice together before the
|
|
2562
|
+
// queued snapshot callback's setImmediate flush runs. Promise-based lstat
|
|
2563
|
+
// otherwise spaces successive rows across I/O turns and splits one logical
|
|
2564
|
+
// warm-up batch into a sequence of one-row captures.
|
|
2565
|
+
if (this.pendingDeleteSnapshotCaptureBatch.length > 0) {
|
|
2566
|
+
this.scheduleDeleteSnapshotCaptureBatch();
|
|
2567
|
+
}
|
|
2452
2568
|
return { batch: { paths, changes }, scannedPaths };
|
|
2453
2569
|
}
|
|
2454
2570
|
/** True while the watch backend is active. */
|