@indigoai-us/hq-cloud 6.15.65 → 6.15.67
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 +30 -2
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +206 -41
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +2 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +260 -2
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/sync/event-sync.d.ts +6 -0
- package/dist/sync/event-sync.d.ts.map +1 -1
- package/dist/sync/event-sync.js +3 -0
- package/dist/sync/event-sync.js.map +1 -1
- package/dist/sync/push-receiver.d.ts +16 -1
- package/dist/sync/push-receiver.d.ts.map +1 -1
- package/dist/sync/push-receiver.js +23 -8
- package/dist/sync/push-receiver.js.map +1 -1
- package/dist/sync/push-receiver.test.js +50 -0
- package/dist/sync/push-receiver.test.js.map +1 -1
- package/dist/watcher.d.ts +18 -2
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +139 -2
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +63 -7
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type TreeChange } from "../watcher.js";
|
|
1
|
+
import { type TreeChange, type TreeChangeBatch } from "../watcher.js";
|
|
2
2
|
import { readJournal } from "../journal.js";
|
|
3
3
|
import type { TelemetryClaims } from "../telemetry-events.js";
|
|
4
4
|
import type { Direction, RunnerLoopDeps, RunnerPassOutcome } from "./sync-runner.js";
|
|
@@ -21,15 +21,37 @@ export declare const EVENT_BATCH_LIMIT_ENV = "HQ_SYNC_EVENT_BATCH_LIMIT";
|
|
|
21
21
|
*/
|
|
22
22
|
export declare const FAST_LANE_MAX_FILES_ENV = "HQ_SYNC_FAST_LANE_MAX_FILES";
|
|
23
23
|
export declare const FAST_LANE_MAX_BYTES_ENV = "HQ_SYNC_FAST_LANE_MAX_BYTES";
|
|
24
|
+
export declare const FAST_LANE_MAX_FILE_BYTES_ENV = "HQ_SYNC_FAST_LANE_MAX_FILE_BYTES";
|
|
24
25
|
export declare const DEFAULT_FAST_LANE_MAX_FILES = 32;
|
|
25
26
|
export declare const DEFAULT_FAST_LANE_MAX_BYTES: number;
|
|
27
|
+
/**
|
|
28
|
+
* A per-file ceiling keeps a single medium-sized transfer from consuming the
|
|
29
|
+
* realtime lane even when the batch aggregate is otherwise below its cap.
|
|
30
|
+
* 256 KiB comfortably covers ordinary notes and control-plane artifacts while
|
|
31
|
+
* leaving session logs and other transfer-heavy files in the slow lane.
|
|
32
|
+
*/
|
|
33
|
+
export declare const DEFAULT_FAST_LANE_MAX_FILE_BYTES: number;
|
|
26
34
|
export declare function resolveFastLaneLimits(): {
|
|
27
35
|
maxFiles: number;
|
|
28
36
|
maxBytes: number;
|
|
37
|
+
maxFileBytes: number;
|
|
38
|
+
};
|
|
39
|
+
/** One latest-per-path work item selected into the realtime or bulk lane. */
|
|
40
|
+
export interface LanePartition<T> {
|
|
41
|
+
fast: T[];
|
|
42
|
+
slow: T[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Partition one selected watcher batch. The returned batches retain the same
|
|
46
|
+
* route/filter metadata, but no latest path can appear in both lane maps.
|
|
47
|
+
*/
|
|
48
|
+
export declare function partitionLocalBatch(batch: TreeChangeBatch): {
|
|
49
|
+
fast: TreeChangeBatch | null;
|
|
50
|
+
slow: TreeChangeBatch | null;
|
|
29
51
|
};
|
|
30
52
|
/** A missing/stat-failed path is never allowed to bypass the slow lane. */
|
|
31
53
|
export declare function isFastLocalBatch(paths: Iterable<string>, changes?: ReadonlyMap<string, Pick<TreeChange, "kind">>): boolean;
|
|
32
|
-
/**
|
|
54
|
+
/** A receiver event needs a known small upsert size before it can go realtime. */
|
|
33
55
|
export declare function isFastReceiverBatch(eventCount: number): boolean;
|
|
34
56
|
export declare function resolveEventBatchLimit(env?: Record<string, string | undefined>): number;
|
|
35
57
|
/**
|
|
@@ -54,6 +76,12 @@ export declare const WATCHER_REDISCOVERY_RETRY_MAX_MS: number;
|
|
|
54
76
|
* can be unit-tested without a real host or a 10-minute wait.
|
|
55
77
|
*/
|
|
56
78
|
export declare function adaptivePollMs(load1: number, cpuCount: number, floorMs?: number, ceilMs?: number): number;
|
|
79
|
+
/**
|
|
80
|
+
* A receiver that has observed messages owns a bounded drain, even if its
|
|
81
|
+
* slow lane is currently waiting for the operation lock. Do not stretch the
|
|
82
|
+
* cadence from that observed work to the CPU-derived backoff ceiling.
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveWatchPollMs(explicitPollMs: number | undefined, receiverHasQueuedWork: boolean, loadAvg: number, cpuCount: number): number;
|
|
57
85
|
/**
|
|
58
86
|
* Advance the cadence from its previous monotonic deadline while the runner is
|
|
59
87
|
* on time. When a pass overruns its slot, give the host one complete interval
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-runner-watch-loop.d.ts","sourceRoot":"","sources":["../../src/bin/sync-runner-watch-loop.ts"],"names":[],"mappings":"AAkBA,OAAO,EAKL,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"sync-runner-watch-loop.d.ts","sourceRoot":"","sources":["../../src/bin/sync-runner-watch-loop.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,7 +12,7 @@ import { PARTIAL_SYNC_EXIT } from "../lib/exit-codes.js";
|
|
|
12
12
|
import { getOrCreateMachineId } from "../lib/machine-id.js";
|
|
13
13
|
import { localPathForVaultKey } from "../local-path-codec.js";
|
|
14
14
|
import { TreeWatcher, resolveEventDebounceConfig, systemClock, } from "../watcher.js";
|
|
15
|
-
import { clearLocalDeleteIntent, markLocalDeleteIntent, PERSONAL_VAULT_JOURNAL_SLUG, openJournalStoreSession, listJournals, readJournal, writeJournal, } from "../journal.js";
|
|
15
|
+
import { clearLocalDeleteIntent, getStateDir, markLocalDeleteIntent, PERSONAL_VAULT_JOURNAL_SLUG, openJournalStoreSession, listJournals, readJournal, writeJournal, } from "../journal.js";
|
|
16
16
|
import { NoopPushReceiver, } from "../sync/push-receiver.js";
|
|
17
17
|
import { resolveEventSync, startEventSync as defaultStartEventSync, } from "../sync/event-sync.js";
|
|
18
18
|
import { buildRoutePullArgv, buildScopedPushArgv, buildScopedDrainPullArgv, buildTargetedPullArgv, isWatchRouteSelected, routeChangeToTarget, routeKey, } from "./sync-runner-watch-routes.js";
|
|
@@ -52,8 +52,16 @@ export const EVENT_BATCH_LIMIT_ENV = "HQ_SYNC_EVENT_BATCH_LIMIT";
|
|
|
52
52
|
*/
|
|
53
53
|
export const FAST_LANE_MAX_FILES_ENV = "HQ_SYNC_FAST_LANE_MAX_FILES";
|
|
54
54
|
export const FAST_LANE_MAX_BYTES_ENV = "HQ_SYNC_FAST_LANE_MAX_BYTES";
|
|
55
|
+
export const FAST_LANE_MAX_FILE_BYTES_ENV = "HQ_SYNC_FAST_LANE_MAX_FILE_BYTES";
|
|
55
56
|
export const DEFAULT_FAST_LANE_MAX_FILES = 32;
|
|
56
57
|
export const DEFAULT_FAST_LANE_MAX_BYTES = 4 * 1024 * 1024;
|
|
58
|
+
/**
|
|
59
|
+
* A per-file ceiling keeps a single medium-sized transfer from consuming the
|
|
60
|
+
* realtime lane even when the batch aggregate is otherwise below its cap.
|
|
61
|
+
* 256 KiB comfortably covers ordinary notes and control-plane artifacts while
|
|
62
|
+
* leaving session logs and other transfer-heavy files in the slow lane.
|
|
63
|
+
*/
|
|
64
|
+
export const DEFAULT_FAST_LANE_MAX_FILE_BYTES = 256 * 1024;
|
|
57
65
|
function positiveEnvInt(name, fallback) {
|
|
58
66
|
const value = Number.parseInt(process.env[name] ?? "", 10);
|
|
59
67
|
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
@@ -62,7 +70,68 @@ export function resolveFastLaneLimits() {
|
|
|
62
70
|
return {
|
|
63
71
|
maxFiles: positiveEnvInt(FAST_LANE_MAX_FILES_ENV, DEFAULT_FAST_LANE_MAX_FILES),
|
|
64
72
|
maxBytes: positiveEnvInt(FAST_LANE_MAX_BYTES_ENV, DEFAULT_FAST_LANE_MAX_BYTES),
|
|
73
|
+
maxFileBytes: positiveEnvInt(FAST_LANE_MAX_FILE_BYTES_ENV, DEFAULT_FAST_LANE_MAX_FILE_BYTES),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Select a bounded realtime prefix from already coalesced work. A path appears
|
|
78
|
+
* exactly once in this input, so a later change to the same path has already
|
|
79
|
+
* replaced its earlier revision and cannot straddle lanes.
|
|
80
|
+
*/
|
|
81
|
+
function partitionByKnownSizes(items, sizeOf) {
|
|
82
|
+
const { maxFiles, maxBytes, maxFileBytes } = resolveFastLaneLimits();
|
|
83
|
+
const fast = [];
|
|
84
|
+
const slow = [];
|
|
85
|
+
let bytes = 0;
|
|
86
|
+
for (const item of items) {
|
|
87
|
+
const size = sizeOf(item);
|
|
88
|
+
if (size === null ||
|
|
89
|
+
size > maxFileBytes ||
|
|
90
|
+
fast.length >= maxFiles ||
|
|
91
|
+
bytes + size > maxBytes) {
|
|
92
|
+
slow.push(item);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
fast.push(item);
|
|
96
|
+
bytes += size;
|
|
97
|
+
}
|
|
98
|
+
return { fast, slow };
|
|
99
|
+
}
|
|
100
|
+
function localPathSize(absolutePath, changes) {
|
|
101
|
+
// `unlinkDir` has an unbounded journal expansion; never guess its payload.
|
|
102
|
+
if (changes?.get(absolutePath)?.kind === "unlinkDir")
|
|
103
|
+
return null;
|
|
104
|
+
try {
|
|
105
|
+
const stat = fs.lstatSync(absolutePath);
|
|
106
|
+
if (stat.isDirectory())
|
|
107
|
+
return null;
|
|
108
|
+
return stat.isFile() ? stat.size : 0;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
// A missing file is an exact delete and has no content transfer. Any other
|
|
112
|
+
// stat error leaves its payload unknown and therefore slow.
|
|
113
|
+
return err.code === "ENOENT" ? 0 : null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Partition one selected watcher batch. The returned batches retain the same
|
|
118
|
+
* route/filter metadata, but no latest path can appear in both lane maps.
|
|
119
|
+
*/
|
|
120
|
+
export function partitionLocalBatch(batch) {
|
|
121
|
+
const partition = partitionByKnownSizes([...batch.paths.entries()], ([absolutePath]) => localPathSize(absolutePath, batch.changes));
|
|
122
|
+
const makeBatch = (entries) => {
|
|
123
|
+
if (entries.length === 0)
|
|
124
|
+
return null;
|
|
125
|
+
const paths = new Map(entries);
|
|
126
|
+
const changes = new Map();
|
|
127
|
+
for (const [absolutePath] of entries) {
|
|
128
|
+
const change = batch.changes?.get(absolutePath);
|
|
129
|
+
if (change)
|
|
130
|
+
changes.set(absolutePath, change);
|
|
131
|
+
}
|
|
132
|
+
return { ...batch, paths, ...(changes.size > 0 ? { changes } : {}) };
|
|
65
133
|
};
|
|
134
|
+
return { fast: makeBatch(partition.fast), slow: makeBatch(partition.slow) };
|
|
66
135
|
}
|
|
67
136
|
/** A missing/stat-failed path is never allowed to bypass the slow lane. */
|
|
68
137
|
export function isFastLocalBatch(paths, changes) {
|
|
@@ -97,7 +166,7 @@ export function isFastLocalBatch(paths, changes) {
|
|
|
97
166
|
}
|
|
98
167
|
return files > 0;
|
|
99
168
|
}
|
|
100
|
-
/**
|
|
169
|
+
/** A receiver event needs a known small upsert size before it can go realtime. */
|
|
101
170
|
export function isFastReceiverBatch(eventCount) {
|
|
102
171
|
return eventCount > 0 && eventCount <= resolveFastLaneLimits().maxFiles;
|
|
103
172
|
}
|
|
@@ -161,6 +230,18 @@ export function adaptivePollMs(load1, cpuCount, floorMs = ADAPTIVE_POLL_FLOOR_MS
|
|
|
161
230
|
const ratio = Math.max(0, Math.min(1, saturation));
|
|
162
231
|
return Math.round(floorMs + ratio * (ceilMs - floorMs));
|
|
163
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* A receiver that has observed messages owns a bounded drain, even if its
|
|
235
|
+
* slow lane is currently waiting for the operation lock. Do not stretch the
|
|
236
|
+
* cadence from that observed work to the CPU-derived backoff ceiling.
|
|
237
|
+
*/
|
|
238
|
+
export function resolveWatchPollMs(explicitPollMs, receiverHasQueuedWork, loadAvg, cpuCount) {
|
|
239
|
+
if (explicitPollMs !== undefined)
|
|
240
|
+
return explicitPollMs;
|
|
241
|
+
return receiverHasQueuedWork
|
|
242
|
+
? 60_000
|
|
243
|
+
: adaptivePollMs(loadAvg, cpuCount);
|
|
244
|
+
}
|
|
164
245
|
/**
|
|
165
246
|
* Advance the cadence from its previous monotonic deadline while the runner is
|
|
166
247
|
* on time. When a pass overruns its slot, give the host one complete interval
|
|
@@ -557,6 +638,19 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
557
638
|
resolveStoppedQueue();
|
|
558
639
|
return;
|
|
559
640
|
}
|
|
641
|
+
// A queued slow pass is the bulk-drain progress boundary. Admit it before
|
|
642
|
+
// refilling the realtime queue; otherwise a steady stream of fast work
|
|
643
|
+
// perpetually leapfrogs a receiver's bulk batch and its SQS poll loop
|
|
644
|
+
// cannot issue another receive.
|
|
645
|
+
if (activeSlowPass === null &&
|
|
646
|
+
activeFastPass === null &&
|
|
647
|
+
pendingSlowPasses.length > 0) {
|
|
648
|
+
const slow = pendingSlowPasses.shift();
|
|
649
|
+
if (slow) {
|
|
650
|
+
const current = startGuardedTask(slow.task, "slow");
|
|
651
|
+
void current.then(slow.resolve, slow.reject);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
560
654
|
if (activeFastPass === null) {
|
|
561
655
|
const fast = pendingFastPasses.shift();
|
|
562
656
|
if (fast) {
|
|
@@ -905,6 +999,22 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
905
999
|
: relativePath.slice(prefix.length + 1),
|
|
906
1000
|
};
|
|
907
1001
|
};
|
|
1002
|
+
/**
|
|
1003
|
+
* A receiver event is eligible only when its upsert bytes are known small.
|
|
1004
|
+
* Current publishers do not require a size field, so a locally journaled
|
|
1005
|
+
* version is the backwards-compatible HEAD-size source. An event with no
|
|
1006
|
+
* declared or journal-known size is bulk: guessing would let a large legacy
|
|
1007
|
+
* transfer consume the realtime lane.
|
|
1008
|
+
*/
|
|
1009
|
+
const receiverEventSize = (event) => {
|
|
1010
|
+
if (event.kind !== "upsert")
|
|
1011
|
+
return null;
|
|
1012
|
+
const coordinates = journalCoordinates(event.relativePath);
|
|
1013
|
+
if (!coordinates)
|
|
1014
|
+
return null;
|
|
1015
|
+
const size = readJournal(coordinates.slug).files[coordinates.key]?.size;
|
|
1016
|
+
return Number.isSafeInteger(size) && size >= 0 ? size : null;
|
|
1017
|
+
};
|
|
908
1018
|
const captureLocalDeleteSnapshots = (relativePath, kind) => {
|
|
909
1019
|
const coordinates = journalCoordinates(relativePath);
|
|
910
1020
|
const route = routeChangeToTarget(relativePath);
|
|
@@ -1030,6 +1140,27 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1030
1140
|
}
|
|
1031
1141
|
return paths;
|
|
1032
1142
|
};
|
|
1143
|
+
const publishedContentHashBaseline = () => {
|
|
1144
|
+
const hashes = {};
|
|
1145
|
+
for (const { slug, journal } of listJournals()) {
|
|
1146
|
+
const isPersonal = slug === PERSONAL_VAULT_JOURNAL_SLUG || slug === "personal";
|
|
1147
|
+
const route = isPersonal
|
|
1148
|
+
? { kind: "personal" }
|
|
1149
|
+
: { kind: "company", slug };
|
|
1150
|
+
if (!isWatchRouteSelected(route, watchRouteSelection))
|
|
1151
|
+
continue;
|
|
1152
|
+
for (const [key, entry] of Object.entries(journal.files)) {
|
|
1153
|
+
// Tombstones and link records have no live regular-file body for the
|
|
1154
|
+
// PushEventEmitter's SHA-256 comparison, so they must not seed it.
|
|
1155
|
+
if (entry.removedAt !== undefined ||
|
|
1156
|
+
entry.kind === "symlink" ||
|
|
1157
|
+
entry.localDiverges)
|
|
1158
|
+
continue;
|
|
1159
|
+
hashes[isPersonal ? key : `companies/${slug}/${key}`] = `sha256:${entry.hash}`;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return hashes;
|
|
1163
|
+
};
|
|
1033
1164
|
const prepareWatcherChanges = (changes) => {
|
|
1034
1165
|
const journals = new Map();
|
|
1035
1166
|
const dirty = new Set();
|
|
@@ -1266,6 +1397,12 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1266
1397
|
return worstExit;
|
|
1267
1398
|
};
|
|
1268
1399
|
let receiver = null;
|
|
1400
|
+
let receiverHasQueuedWork = false;
|
|
1401
|
+
const onReceiveActivity = (hasMessages) => {
|
|
1402
|
+
receiverHasQueuedWork = hasMessages;
|
|
1403
|
+
if (hasMessages)
|
|
1404
|
+
interruptWait();
|
|
1405
|
+
};
|
|
1269
1406
|
let eventSync = null;
|
|
1270
1407
|
const realtimeState = { scheduler: null, nextHighWaterAt: null };
|
|
1271
1408
|
function activateEventPushSurfaces(rediscovery = false) {
|
|
@@ -1342,29 +1479,34 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1342
1479
|
watcher.onChange((changedRelPath, batch) => {
|
|
1343
1480
|
if (stopped)
|
|
1344
1481
|
return;
|
|
1345
|
-
//
|
|
1346
|
-
// bulk pass is in flight.
|
|
1347
|
-
//
|
|
1482
|
+
// Peel a bounded realtime sub-batch from a mixed watcher batch while a
|
|
1483
|
+
// bulk pass is in flight. The residue enters the ordinary pending drain
|
|
1484
|
+
// immediately; a latest path appears in exactly one of these maps.
|
|
1348
1485
|
const selectedBatch = batch ? selectWatchBatch(batch) : null;
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1486
|
+
const lanes = selectedBatch && !selectedBatch.overflowed
|
|
1487
|
+
? partitionLocalBatch(selectedBatch)
|
|
1488
|
+
: null;
|
|
1489
|
+
if (lanes?.fast &&
|
|
1490
|
+
activeSlowPass !== null) {
|
|
1353
1491
|
const generation = ++nextWatcherBatchGeneration;
|
|
1354
1492
|
// Fast dispatch bypasses the pending-batch ingress, so preserve both
|
|
1355
1493
|
// of its contracts here: only selected routes may run, and V2 sees
|
|
1356
1494
|
// the same dirty signal as the compatibility drain.
|
|
1357
1495
|
realtimeState.scheduler?.signal("watcher");
|
|
1358
|
-
void runScopedDrain(
|
|
1496
|
+
void runScopedDrain(lanes.fast, generation, true).then((exit) => {
|
|
1359
1497
|
if (exit === 0)
|
|
1360
1498
|
return;
|
|
1361
|
-
restorePendingWatcherPaths(
|
|
1499
|
+
restorePendingWatcherPaths(lanes.fast.paths, generation);
|
|
1362
1500
|
interruptWait();
|
|
1363
1501
|
}).catch((err) => {
|
|
1364
|
-
restorePendingWatcherPaths(
|
|
1502
|
+
restorePendingWatcherPaths(lanes.fast.paths, generation);
|
|
1365
1503
|
process.stderr.write(`watch fast-lane scoped push failed, will retry next tick: ${describeError(err)}\n`);
|
|
1366
1504
|
interruptWait();
|
|
1367
1505
|
});
|
|
1506
|
+
if (lanes.slow && addPendingWatcherChange(undefined, lanes.slow)) {
|
|
1507
|
+
realtimeState.scheduler?.signal("watcher");
|
|
1508
|
+
interruptWait();
|
|
1509
|
+
}
|
|
1368
1510
|
return;
|
|
1369
1511
|
}
|
|
1370
1512
|
if (addPendingWatcherChange(changedRelPath, batch)) {
|
|
@@ -1454,40 +1596,54 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1454
1596
|
const receiverSyncBatchFn = async ({ events }) => {
|
|
1455
1597
|
if (stopped)
|
|
1456
1598
|
return;
|
|
1599
|
+
// The injectable receiver seam and the production SQS receiver both
|
|
1600
|
+
// arrive here only after a successful receive. Keep the cadence tight
|
|
1601
|
+
// until the receiver later reports an empty outer long-poll.
|
|
1602
|
+
onReceiveActivity(true);
|
|
1457
1603
|
// A received wake/high-water signal means there is bounded work now.
|
|
1458
1604
|
// It wakes the scheduler but is still funneled through runGuarded below,
|
|
1459
1605
|
// so it cannot overlap the pass that may currently own the operation
|
|
1460
1606
|
// lock.
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
// cap to its whole eligible payload, not independently to each route.
|
|
1476
|
-
const lane = isFastReceiverBatch(eligibleEventCount)
|
|
1477
|
-
? "fast"
|
|
1478
|
-
: "slow";
|
|
1479
|
-
for (const { route, paths } of pathsByRoute.values()) {
|
|
1480
|
-
// One guarded targeted pass per affected route, with every coalesced
|
|
1481
|
-
// path for that route expressed through the watcher's existing argv seam.
|
|
1482
|
-
const targetedArgv = buildTargetedPullArgv(route, passArgv, paths);
|
|
1483
|
-
const result = await runGuarded(targetedArgv, undefined, lane);
|
|
1484
|
-
if (passExitCode(result) !== 0) {
|
|
1485
|
-
throw new Error(`targeted pull failed with exit code ${passExitCode(result)}`);
|
|
1607
|
+
// Apply the caps once across the whole delivery, then retain the residue
|
|
1608
|
+
// in the normal lane. The receiver already coalesces to one latest event
|
|
1609
|
+
// per path, so the partition cannot split revisions of one path.
|
|
1610
|
+
const lanes = partitionByKnownSizes(events, receiverEventSize);
|
|
1611
|
+
const drainLane = async (lane, laneEvents) => {
|
|
1612
|
+
const pathsByRoute = new Map();
|
|
1613
|
+
for (const event of laneEvents) {
|
|
1614
|
+
const route = routeChangeToTarget(event.relativePath);
|
|
1615
|
+
if (!route)
|
|
1616
|
+
continue;
|
|
1617
|
+
const key = routeKey(route);
|
|
1618
|
+
const group = pathsByRoute.get(key) ?? { route, paths: [] };
|
|
1619
|
+
group.paths.push(event.relativePath);
|
|
1620
|
+
pathsByRoute.set(key, group);
|
|
1486
1621
|
}
|
|
1487
|
-
|
|
1622
|
+
for (const { route, paths } of pathsByRoute.values()) {
|
|
1623
|
+
const targetedArgv = buildTargetedPullArgv(route, passArgv, paths);
|
|
1624
|
+
const result = await runGuarded(targetedArgv, undefined, lane);
|
|
1625
|
+
if (passExitCode(result) !== 0) {
|
|
1626
|
+
throw new Error(`targeted pull failed with exit code ${passExitCode(result)}`);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
// Dispatch realtime work before awaiting its bulk residue. This lets an
|
|
1631
|
+
// established slow pass continue while the small scoped pull uses its
|
|
1632
|
+
// ref-counted lock carve-out; the residue remains serialized and cannot
|
|
1633
|
+
// starve the slow lane.
|
|
1634
|
+
await drainLane("fast", lanes.fast);
|
|
1635
|
+
await drainLane("slow", lanes.slow);
|
|
1636
|
+
};
|
|
1637
|
+
receiverSyncBatchFn.partition = (events) => {
|
|
1638
|
+
const lanes = partitionByKnownSizes(events, receiverEventSize);
|
|
1639
|
+
return [lanes.fast, lanes.slow].filter((lane) => lane.length > 0);
|
|
1488
1640
|
};
|
|
1489
1641
|
const createReceiver = deps.createReceiver ?? (() => new NoopPushReceiver());
|
|
1490
|
-
receiver = createReceiver({
|
|
1642
|
+
receiver = createReceiver({
|
|
1643
|
+
syncBatchFn: receiverSyncBatchFn,
|
|
1644
|
+
hqRoot,
|
|
1645
|
+
onReceiveActivity,
|
|
1646
|
+
});
|
|
1491
1647
|
void Promise.resolve(receiver.start()).catch(() => undefined);
|
|
1492
1648
|
if (deps.startRealtimeScheduler) {
|
|
1493
1649
|
// Bound the very first post-maintenance wait while the async scheduler
|
|
@@ -1562,6 +1718,9 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1562
1718
|
log: (m) => process.stderr.write(`${m}\n`),
|
|
1563
1719
|
telemetryClient: eventSyncClient,
|
|
1564
1720
|
telemetryClaims: claims,
|
|
1721
|
+
publishedContentHashStateDir: getStateDir(),
|
|
1722
|
+
initialPublishedContentHashes: publishedContentHashBaseline(),
|
|
1723
|
+
onReceiveActivity,
|
|
1565
1724
|
});
|
|
1566
1725
|
if (!handles)
|
|
1567
1726
|
return;
|
|
@@ -1807,7 +1966,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1807
1966
|
process.stderr.write(`hq-sync-runner: backup retention dry-run failed — ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1808
1967
|
}
|
|
1809
1968
|
}
|
|
1810
|
-
const nextPollMs = explicitPollMs ??
|
|
1969
|
+
const nextPollMs = explicitPollMs ?? resolveWatchPollMs(undefined, receiverHasQueuedWork, receiverHasQueuedWork ? 0 : sampleLoadAvg(), cpuCount);
|
|
1811
1970
|
const activeRealtimeScheduler = realtimeState.scheduler;
|
|
1812
1971
|
let nextRealtimeHighWaterAt = realtimeState.nextHighWaterAt;
|
|
1813
1972
|
if (activeRealtimeScheduler !== null && nextRealtimeHighWaterAt !== null && monotonicNow() >= nextRealtimeHighWaterAt) {
|
|
@@ -1820,7 +1979,13 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1820
1979
|
} while (nextRealtimeHighWaterAt <= monotonicNow());
|
|
1821
1980
|
realtimeState.nextHighWaterAt = nextRealtimeHighWaterAt;
|
|
1822
1981
|
}
|
|
1823
|
-
const
|
|
1982
|
+
const now = monotonicNow();
|
|
1983
|
+
// Queued receiver work shortens an already scheduled adaptive poll from
|
|
1984
|
+
// as long as ten minutes to one minute. Its deadline must start at this
|
|
1985
|
+
// receive-driven cycle rather than extending the old, still-future slot.
|
|
1986
|
+
const schedule = receiverHasQueuedWork && explicitPollMs === undefined
|
|
1987
|
+
? { nextDueAt: now + nextPollMs, delayMs: nextPollMs }
|
|
1988
|
+
: advanceMonotonicDeadline(nextDueAt, now, nextPollMs);
|
|
1824
1989
|
nextDueAt = schedule.nextDueAt;
|
|
1825
1990
|
const untilHighWaterMs = realtimeState.nextHighWaterAt === null
|
|
1826
1991
|
? schedule.delayMs
|