@indigoai-us/hq-cloud 6.16.65 → 6.16.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-company.d.ts +14 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +42 -8
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +62 -7
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts +12 -3
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +65 -3
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.js +172 -1
- package/dist/bin/sync-runner-planning.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +51 -2
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +409 -165
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +19 -2
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +40 -5
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.test.js +47 -0
- package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +59 -5
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +48 -4
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +1553 -192
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts +2 -0
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts.map +1 -0
- package/dist/cli/onepass-pull-planner-regressions.test.js +300 -0
- package/dist/cli/onepass-pull-planner-regressions.test.js.map +1 -0
- package/dist/cli/onepass-pull-planner.test.d.ts +2 -0
- package/dist/cli/onepass-pull-planner.test.d.ts.map +1 -0
- package/dist/cli/onepass-pull-planner.test.js +266 -0
- package/dist/cli/onepass-pull-planner.test.js.map +1 -0
- package/dist/cli/push-scan-exclusions.test.js +4 -4
- package/dist/cli/push-scan-exclusions.test.js.map +1 -1
- package/dist/cli/share.d.ts +31 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +143 -8
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +350 -5
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/streaming-pull-planner.bench.test.d.ts +15 -0
- package/dist/cli/streaming-pull-planner.bench.test.d.ts.map +1 -0
- package/dist/cli/streaming-pull-planner.bench.test.js +273 -0
- package/dist/cli/streaming-pull-planner.bench.test.js.map +1 -0
- package/dist/cli/sync-scope.test.js +8 -0
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +12 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +297 -125
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +255 -2
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal-row-store.d.ts +40 -5
- package/dist/journal-row-store.d.ts.map +1 -1
- package/dist/journal-row-store.js +240 -21
- package/dist/journal-row-store.js.map +1 -1
- package/dist/journal-row-store.test.js +54 -0
- package/dist/journal-row-store.test.js.map +1 -1
- package/dist/journal-snapshot-v4.d.ts.map +1 -1
- package/dist/journal-snapshot-v4.js +11 -6
- package/dist/journal-snapshot-v4.js.map +1 -1
- package/dist/journal-store.test.js +324 -7
- package/dist/journal-store.test.js.map +1 -1
- package/dist/journal.d.ts +51 -16
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +303 -79
- package/dist/journal.js.map +1 -1
- package/dist/lib/exit-codes.d.ts +15 -0
- package/dist/lib/exit-codes.d.ts.map +1 -1
- package/dist/lib/exit-codes.js +15 -0
- package/dist/lib/exit-codes.js.map +1 -1
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +10 -3
- package/dist/personal-vault.js.map +1 -1
- package/dist/personal-vault.test.js +35 -0
- package/dist/personal-vault.test.js.map +1 -1
- package/dist/remote-pull.d.ts.map +1 -1
- package/dist/remote-pull.js +9 -2
- package/dist/remote-pull.js.map +1 -1
- package/dist/remote-pull.test.js +59 -2
- package/dist/remote-pull.test.js.map +1 -1
- package/dist/s3.d.ts +1 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +20 -2
- package/dist/s3.js.map +1 -1
- package/dist/s3.list-cost.test.d.ts +2 -0
- package/dist/s3.list-cost.test.d.ts.map +1 -0
- package/dist/s3.list-cost.test.js +51 -0
- package/dist/s3.list-cost.test.js.map +1 -0
- package/dist/sync/area-ledger.d.ts +27 -3
- package/dist/sync/area-ledger.d.ts.map +1 -1
- package/dist/sync/area-ledger.js +149 -39
- package/dist/sync/area-ledger.js.map +1 -1
- package/dist/sync/area-ledger.test.js +192 -5
- package/dist/sync/area-ledger.test.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.test.js +29 -0
- package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
- package/dist/sync/realtime-telemetry.d.ts +1 -1
- package/dist/sync/realtime-telemetry.d.ts.map +1 -1
- package/dist/sync/realtime-telemetry.js +14 -6
- package/dist/sync/realtime-telemetry.js.map +1 -1
- package/dist/sync/realtime-telemetry.test.js +136 -16
- package/dist/sync/realtime-telemetry.test.js.map +1 -1
- package/dist/sync/remote-file-spool.d.ts +16 -0
- package/dist/sync/remote-file-spool.d.ts.map +1 -0
- package/dist/sync/remote-file-spool.js +91 -0
- package/dist/sync/remote-file-spool.js.map +1 -0
- package/dist/sync/remote-file-spool.test.d.ts +2 -0
- package/dist/sync/remote-file-spool.test.d.ts.map +1 -0
- package/dist/sync/remote-file-spool.test.js +77 -0
- package/dist/sync/remote-file-spool.test.js.map +1 -0
- package/dist/sync/state-store.d.ts +6 -0
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +14 -2
- package/dist/sync/state-store.js.map +1 -1
- package/dist/watcher.d.ts +12 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +131 -0
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +63 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +2 -1
|
@@ -10,12 +10,12 @@ import { pickCanonicalPersonEntity } from "../vault-client.js";
|
|
|
10
10
|
import { acquireOperationLock, withOperationLock, withOperationLockSync, OperationLockedError, OPERATION_LOCKED_EXIT, } from "../operation-lock.js";
|
|
11
11
|
import { describeError } from "../lib/describe-error.js";
|
|
12
12
|
import { TRANSIENT_NETWORK_EXIT } from "../lib/net-errors.js";
|
|
13
|
-
import { PARTIAL_SYNC_EXIT } from "../lib/exit-codes.js";
|
|
13
|
+
import { PARTIAL_SYNC_EXIT, UNRESOLVED_COMPANY_EXIT, } from "../lib/exit-codes.js";
|
|
14
14
|
import { getOrCreateMachineId } from "../lib/machine-id.js";
|
|
15
15
|
import { localPathForVaultKey } from "../local-path-codec.js";
|
|
16
16
|
import { TreeWatcher, resolveEventDebounceConfig, systemClock, } from "../watcher.js";
|
|
17
17
|
import { recoverHqRootLocalState } from "../hq-root-recovery.js";
|
|
18
|
-
import { clearLocalDeleteIntent, getStateDir, markLocalDeleteIntent, PERSONAL_VAULT_JOURNAL_SLUG, openJournalStoreSession, openJournalStoreSessionAsync, listJournalSlugs, listJournals, maintainAreaJournalSlice,
|
|
18
|
+
import { clearLocalDeleteIntent, getStateDir, markLocalDeleteIntent, PERSONAL_VAULT_JOURNAL_SLUG, openJournalStoreSession, openJournalStoreSessionAsync, listJournalSlugs, listJournals, maintainAreaJournalSlice, disposeScopedJournalStores, releaseScopedJournalStore, releaseCachedAreaJournalMigrations, journalCacheCensusSizes, journalRows, readJournalScoped, writeJournal, } from "../journal.js";
|
|
19
19
|
import { isPersonalVaultDeletionExcluded } from "../personal-vault.js";
|
|
20
20
|
import { NoopPushReceiver, } from "../sync/push-receiver.js";
|
|
21
21
|
import { resolveEventSync, startEventSync as defaultStartEventSync, } from "../sync/event-sync.js";
|
|
@@ -31,6 +31,219 @@ import { InlineSyncWorkerAdapter, } from "../sync/sync-work.js";
|
|
|
31
31
|
import { buildRoutePullArgv, buildScopedPushArgv, buildScopedDrainPullArgv, buildTargetedPullArgv, isWatchRouteSelected, routeChangeToTarget, routeKey, } from "./sync-runner-watch-routes.js";
|
|
32
32
|
import { resolveSkipCompanies } from "./sync-runner-planning.js";
|
|
33
33
|
const FULL_RECONCILE_EVERY_TICKS = 10;
|
|
34
|
+
/** A deferred full chore must get the next admission after this long. */
|
|
35
|
+
export const CHORE_STARVATION_CAP_MS = 15 * 60 * 1000;
|
|
36
|
+
/**
|
|
37
|
+
* One durable-state owner at a time for a full chore. Realtime work never runs
|
|
38
|
+
* beside that chore: it explicitly awaits `checkpoint()` after a completed
|
|
39
|
+
* durable unit, then resumes once the hand-off is finished. A separately
|
|
40
|
+
* tracked, explicitly opted-in watcher fast pass may use the pre-existing
|
|
41
|
+
* scoped fast-over-slow exception, but never while a chore owns durable state.
|
|
42
|
+
*/
|
|
43
|
+
export class CooperativePassScheduler {
|
|
44
|
+
now;
|
|
45
|
+
starvationCapMs;
|
|
46
|
+
pending = [];
|
|
47
|
+
active = null;
|
|
48
|
+
// Scoped fast realtime work has the established in-process lock exception
|
|
49
|
+
// while a scoped slow realtime upload is in flight. Keep that slot separate
|
|
50
|
+
// from `active`: a chore still has exactly one durable-state owner.
|
|
51
|
+
activeFastRealtime = null;
|
|
52
|
+
stopped = false;
|
|
53
|
+
_handoffActive = false;
|
|
54
|
+
constructor(now, starvationCapMs = CHORE_STARVATION_CAP_MS) {
|
|
55
|
+
this.now = now;
|
|
56
|
+
this.starvationCapMs = starvationCapMs;
|
|
57
|
+
}
|
|
58
|
+
get handoffActive() {
|
|
59
|
+
return this._handoffActive;
|
|
60
|
+
}
|
|
61
|
+
get hasActiveChore() {
|
|
62
|
+
return this.active?.workClass === "chore";
|
|
63
|
+
}
|
|
64
|
+
get hasActiveSlowRealtime() {
|
|
65
|
+
return this.active?.workClass === "realtime" && this.active.lane === "slow";
|
|
66
|
+
}
|
|
67
|
+
pendingLaneCount(lane) {
|
|
68
|
+
return this.pending.filter((entry) => entry.lane === lane).length;
|
|
69
|
+
}
|
|
70
|
+
submit(task, workClass, lane = "slow", allowFastRealtimeOvertake = false) {
|
|
71
|
+
if (this.stopped)
|
|
72
|
+
return Promise.resolve(0);
|
|
73
|
+
const directEntry = {
|
|
74
|
+
task,
|
|
75
|
+
workClass,
|
|
76
|
+
lane,
|
|
77
|
+
allowFastRealtimeOvertake,
|
|
78
|
+
// An immediately admitted task cannot starve, so avoid sampling the
|
|
79
|
+
// cadence clock merely to stamp it. This preserves the loop's deadline
|
|
80
|
+
// accounting and keeps the timestamp meaningful only for real queues.
|
|
81
|
+
enqueuedAt: 0,
|
|
82
|
+
resolve: () => undefined,
|
|
83
|
+
reject: () => undefined,
|
|
84
|
+
};
|
|
85
|
+
if (this.canStartFastRealtime(directEntry)) {
|
|
86
|
+
return this.startFastRealtime(directEntry);
|
|
87
|
+
}
|
|
88
|
+
if (this.active === null &&
|
|
89
|
+
this.activeFastRealtime === null &&
|
|
90
|
+
this.pending.length === 0) {
|
|
91
|
+
return this.start(directEntry);
|
|
92
|
+
}
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const entry = { ...directEntry, enqueuedAt: this.now(), resolve, reject };
|
|
95
|
+
this.pending.push(entry);
|
|
96
|
+
this.drain();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
stop() {
|
|
100
|
+
this.stopped = true;
|
|
101
|
+
while (this.pending.length > 0)
|
|
102
|
+
this.pending.shift()?.resolve(0);
|
|
103
|
+
}
|
|
104
|
+
drain() {
|
|
105
|
+
if (this.stopped || this.pending.length === 0)
|
|
106
|
+
return;
|
|
107
|
+
if (this.active !== null) {
|
|
108
|
+
const fastRealtime = this.nextFastRealtime();
|
|
109
|
+
if (fastRealtime && this.canStartFastRealtime(fastRealtime)) {
|
|
110
|
+
this.pending.splice(this.pending.indexOf(fastRealtime), 1);
|
|
111
|
+
this.startFastRealtime(fastRealtime);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (this.activeFastRealtime !== null)
|
|
116
|
+
return;
|
|
117
|
+
const next = this.takeNext();
|
|
118
|
+
if (next)
|
|
119
|
+
this.start(next);
|
|
120
|
+
}
|
|
121
|
+
takeNext() {
|
|
122
|
+
const now = this.now();
|
|
123
|
+
const starvedChore = this.pending.findIndex((entry) => entry.workClass === "chore" && now - entry.enqueuedAt >= this.starvationCapMs);
|
|
124
|
+
if (starvedChore >= 0)
|
|
125
|
+
return this.pending.splice(starvedChore, 1)[0];
|
|
126
|
+
// A receiver's bulk drain is still realtime work, but its slow lane is the
|
|
127
|
+
// progress boundary for that delivery. Preserve its established turn
|
|
128
|
+
// before refilling later fast realtime arrivals; this does not let a
|
|
129
|
+
// chore overtake any realtime work.
|
|
130
|
+
const realtime = this.nextRealtimeIndex();
|
|
131
|
+
if (realtime >= 0)
|
|
132
|
+
return this.pending.splice(realtime, 1)[0];
|
|
133
|
+
return this.pending.shift();
|
|
134
|
+
}
|
|
135
|
+
nextRealtimeIndex() {
|
|
136
|
+
const slowRealtime = this.pending.findIndex((entry) => entry.workClass === "realtime" && entry.lane === "slow");
|
|
137
|
+
return slowRealtime >= 0
|
|
138
|
+
? slowRealtime
|
|
139
|
+
: this.pending.findIndex((entry) => entry.workClass === "realtime");
|
|
140
|
+
}
|
|
141
|
+
nextFastRealtime() {
|
|
142
|
+
return this.pending.find((entry) => entry.workClass === "realtime" && entry.lane === "fast");
|
|
143
|
+
}
|
|
144
|
+
hasStarvedChore() {
|
|
145
|
+
const now = this.now();
|
|
146
|
+
return this.pending.some((entry) => entry.workClass === "chore" && now - entry.enqueuedAt >= this.starvationCapMs);
|
|
147
|
+
}
|
|
148
|
+
canStartFastRealtime(entry) {
|
|
149
|
+
return (!this._handoffActive &&
|
|
150
|
+
this.activeFastRealtime === null &&
|
|
151
|
+
entry.workClass === "realtime" &&
|
|
152
|
+
entry.lane === "fast" &&
|
|
153
|
+
entry.allowFastRealtimeOvertake &&
|
|
154
|
+
this.hasActiveSlowRealtime &&
|
|
155
|
+
!this.hasStarvedChore());
|
|
156
|
+
}
|
|
157
|
+
start(entry) {
|
|
158
|
+
this.active = entry;
|
|
159
|
+
let current;
|
|
160
|
+
try {
|
|
161
|
+
current = this.execute(entry);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
current = Promise.reject(error);
|
|
165
|
+
}
|
|
166
|
+
void current.then((outcome) => {
|
|
167
|
+
entry.resolve(outcome);
|
|
168
|
+
this.complete(entry);
|
|
169
|
+
}, (error) => {
|
|
170
|
+
entry.reject(error);
|
|
171
|
+
this.complete(entry);
|
|
172
|
+
}).catch(() => undefined);
|
|
173
|
+
return current;
|
|
174
|
+
}
|
|
175
|
+
complete(entry) {
|
|
176
|
+
if (this.active === entry)
|
|
177
|
+
this.active = null;
|
|
178
|
+
this.drain();
|
|
179
|
+
}
|
|
180
|
+
startFastRealtime(entry) {
|
|
181
|
+
this.activeFastRealtime = entry;
|
|
182
|
+
let current;
|
|
183
|
+
try {
|
|
184
|
+
current = this.execute(entry);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
current = Promise.reject(error);
|
|
188
|
+
}
|
|
189
|
+
void current.then((outcome) => {
|
|
190
|
+
entry.resolve(outcome);
|
|
191
|
+
this.completeFastRealtime(entry);
|
|
192
|
+
}, (error) => {
|
|
193
|
+
entry.reject(error);
|
|
194
|
+
this.completeFastRealtime(entry);
|
|
195
|
+
});
|
|
196
|
+
return current;
|
|
197
|
+
}
|
|
198
|
+
completeFastRealtime(entry) {
|
|
199
|
+
if (this.activeFastRealtime === entry)
|
|
200
|
+
this.activeFastRealtime = null;
|
|
201
|
+
this.drain();
|
|
202
|
+
}
|
|
203
|
+
execute(entry) {
|
|
204
|
+
return entry.task({ checkpoint: () => this.checkpoint(entry) });
|
|
205
|
+
}
|
|
206
|
+
async checkpoint(entry) {
|
|
207
|
+
if (this.stopped ||
|
|
208
|
+
entry.workClass !== "chore" ||
|
|
209
|
+
this.active !== entry ||
|
|
210
|
+
!this.pending.some((pending) => pending.workClass === "realtime")) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
// `entry` is suspended at a caller-declared durable boundary. It remains
|
|
214
|
+
// the sole full-pass continuation and will resume after this bounded burst.
|
|
215
|
+
const deferredAt = this.now();
|
|
216
|
+
this.active = null;
|
|
217
|
+
this._handoffActive = true;
|
|
218
|
+
let handedOff = false;
|
|
219
|
+
try {
|
|
220
|
+
while (!this.stopped && this.now() - deferredAt < this.starvationCapMs) {
|
|
221
|
+
const realtimeIndex = this.nextRealtimeIndex();
|
|
222
|
+
if (realtimeIndex < 0)
|
|
223
|
+
break;
|
|
224
|
+
const realtime = this.pending.splice(realtimeIndex, 1)[0];
|
|
225
|
+
handedOff = true;
|
|
226
|
+
this.active = realtime;
|
|
227
|
+
try {
|
|
228
|
+
realtime.resolve(await this.execute(realtime));
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
realtime.reject(error);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
if (this.active === realtime)
|
|
235
|
+
this.active = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
this._handoffActive = false;
|
|
241
|
+
if (!this.stopped)
|
|
242
|
+
this.active = entry;
|
|
243
|
+
}
|
|
244
|
+
return handedOff;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
34
247
|
const MAX_FULL_RECONCILE_REASONS = 7;
|
|
35
248
|
/**
|
|
36
249
|
* Area storage is AUTO by default and only applies after durable cutover.
|
|
@@ -510,6 +723,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
510
723
|
const idleHeartbeatIntervalMs = deps.idleHeartbeatIntervalMs ?? WATCH_IDLE_HEARTBEAT_INTERVAL_MS;
|
|
511
724
|
const monotonicNow = deps.monotonicNow ?? (() => performance.now());
|
|
512
725
|
const runPass = deps.runPass ?? runtime.runPassWithOperationLockAlreadyHeld;
|
|
726
|
+
const runPassSupportsCooperativeCheckpoint = deps.runPass === undefined || deps.runPassSupportsCooperativeCheckpoint === true;
|
|
513
727
|
const pollIdx = argv.indexOf("--poll-remote-ms");
|
|
514
728
|
// An explicit --poll-remote-ms pins a fixed interval (menubar/CLI still set
|
|
515
729
|
// one). When it is omitted, the interval is resolved fresh each cycle from
|
|
@@ -520,7 +734,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
520
734
|
const eventPush = argv.includes("--event-push");
|
|
521
735
|
const fullReconcileIntervalMs = resolveFullReconcileMs();
|
|
522
736
|
const companiesMode = argv.includes("--companies");
|
|
523
|
-
const
|
|
737
|
+
const companyIdx = argv.indexOf("--company");
|
|
738
|
+
const companyMode = companyIdx >= 0;
|
|
739
|
+
// The tenant a single-company run owns. Carried into route selection so a
|
|
740
|
+
// watcher or receiver event under another company cannot queue a pass for it.
|
|
741
|
+
const companySlug = companyMode ? argv[companyIdx + 1] : undefined;
|
|
524
742
|
const personalMode = argv.includes("--personal");
|
|
525
743
|
// Keep this at the watcher ingress rather than importing the runner's
|
|
526
744
|
// resolver: sync-runner imports this loop, and a reverse value import would
|
|
@@ -529,6 +747,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
529
747
|
const watchRouteSelection = {
|
|
530
748
|
companies: companiesMode,
|
|
531
749
|
company: companyMode,
|
|
750
|
+
...(companySlug !== undefined ? { companySlug } : {}),
|
|
532
751
|
personal: personalMode,
|
|
533
752
|
skipPersonal: parsed.skipPersonal === true ||
|
|
534
753
|
skipPersonalEnv === "1" ||
|
|
@@ -673,7 +892,27 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
673
892
|
resolveNoOperationLockUsers = resolve;
|
|
674
893
|
});
|
|
675
894
|
};
|
|
676
|
-
const
|
|
895
|
+
const passScheduler = new CooperativePassScheduler(monotonicNow);
|
|
896
|
+
const runPassWithLock = async (passArgvForRun, prepare, lane = "slow", workClass = "chore", checkpoint) => {
|
|
897
|
+
const reportedAppliedUpserts = new Set();
|
|
898
|
+
const recordDurableAppliedUpserts = (upserts) => {
|
|
899
|
+
for (const applied of upserts) {
|
|
900
|
+
const key = `${applied.relativePath}\u0000${applied.contentHash}`;
|
|
901
|
+
if (reportedAppliedUpserts.has(key))
|
|
902
|
+
continue;
|
|
903
|
+
reportedAppliedUpserts.add(key);
|
|
904
|
+
rememberAppliedUpsert(applied.relativePath, applied.contentHash);
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
const invokePass = () => {
|
|
908
|
+
if (!runPassSupportsCooperativeCheckpoint)
|
|
909
|
+
return runPass(passArgvForRun);
|
|
910
|
+
// This callback is emitted by the production runner after journal
|
|
911
|
+
// repair/seed and before fanout. It starts ingress early, but never
|
|
912
|
+
// admits a second durable writer: queued realtime still waits for the
|
|
913
|
+
// existing scheduler's cooperative hand-off checkpoint.
|
|
914
|
+
return runPass(passArgvForRun, checkpoint, () => activateEventPushSurfaces(), recordDurableAppliedUpserts);
|
|
915
|
+
};
|
|
677
916
|
// Observe the heap at pass boundaries — a gated checkpoint immediately before
|
|
678
917
|
// admitting a pass and immediately after it returns. A pass that runs a long
|
|
679
918
|
// synchronous stretch starves the interval timer, so these boundary
|
|
@@ -685,9 +924,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
685
924
|
heapCheckpoint?.();
|
|
686
925
|
if (typeof outcome === "number")
|
|
687
926
|
return;
|
|
688
|
-
|
|
689
|
-
rememberAppliedUpsert(applied.relativePath, applied.contentHash);
|
|
690
|
-
}
|
|
927
|
+
recordDurableAppliedUpserts(outcome.result?.appliedUpserts ?? []);
|
|
691
928
|
};
|
|
692
929
|
// Pre-admit boundary observation.
|
|
693
930
|
heapCheckpoint?.();
|
|
@@ -696,13 +933,31 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
696
933
|
// its keyed journal delta and per-object mutation are independent from the
|
|
697
934
|
// slow pass's bulk paths, and the slow pass already owns the root lock.
|
|
698
935
|
// Never bypass the lock when no slow pass is active.
|
|
936
|
+
if (
|
|
937
|
+
// A full chore is paused at a caller-declared durable checkpoint. Keep
|
|
938
|
+
// the root lock held by this process, but lend it to exactly one
|
|
939
|
+
// realtime pass; the suspended chore cannot mutate until this returns.
|
|
940
|
+
workClass === "realtime" &&
|
|
941
|
+
passScheduler.handoffActive &&
|
|
942
|
+
processOwnsOperationLock) {
|
|
943
|
+
acquireOperationLockUser();
|
|
944
|
+
try {
|
|
945
|
+
prepare?.();
|
|
946
|
+
const outcome = await invokePass();
|
|
947
|
+
recordAppliedUpserts(outcome);
|
|
948
|
+
return outcome;
|
|
949
|
+
}
|
|
950
|
+
finally {
|
|
951
|
+
releaseOperationLockUser();
|
|
952
|
+
}
|
|
953
|
+
}
|
|
699
954
|
if (lane === "fast" &&
|
|
700
955
|
processOwnsOperationLock &&
|
|
701
956
|
coordinatorWholeMutationChores === 0) {
|
|
702
957
|
acquireOperationLockUser();
|
|
703
958
|
try {
|
|
704
959
|
prepare?.();
|
|
705
|
-
const outcome = await
|
|
960
|
+
const outcome = await invokePass();
|
|
706
961
|
recordAppliedUpserts(outcome);
|
|
707
962
|
return outcome;
|
|
708
963
|
}
|
|
@@ -722,7 +977,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
722
977
|
acquireOperationLockUser();
|
|
723
978
|
}
|
|
724
979
|
prepare?.();
|
|
725
|
-
const outcome = await
|
|
980
|
+
const outcome = await invokePass();
|
|
726
981
|
recordAppliedUpserts(outcome);
|
|
727
982
|
return outcome;
|
|
728
983
|
}
|
|
@@ -756,7 +1011,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
756
1011
|
}
|
|
757
1012
|
try {
|
|
758
1013
|
prepare?.();
|
|
759
|
-
const outcome = await
|
|
1014
|
+
const outcome = await invokePass();
|
|
760
1015
|
recordAppliedUpserts(outcome);
|
|
761
1016
|
return outcome;
|
|
762
1017
|
}
|
|
@@ -779,7 +1034,18 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
779
1034
|
throw err;
|
|
780
1035
|
}
|
|
781
1036
|
};
|
|
782
|
-
const runTaskWithLock = async (task) => {
|
|
1037
|
+
const runTaskWithLock = async (task, workClass = "chore") => {
|
|
1038
|
+
if (workClass === "realtime" &&
|
|
1039
|
+
passScheduler.handoffActive &&
|
|
1040
|
+
processOwnsOperationLock) {
|
|
1041
|
+
acquireOperationLockUser();
|
|
1042
|
+
try {
|
|
1043
|
+
return await task();
|
|
1044
|
+
}
|
|
1045
|
+
finally {
|
|
1046
|
+
releaseOperationLockUser();
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
783
1049
|
try {
|
|
784
1050
|
const handle = acquireOperationLock(hqRoot, "sync", {
|
|
785
1051
|
timeoutSec: 0,
|
|
@@ -873,102 +1139,15 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
873
1139
|
heapCheckpoint?.();
|
|
874
1140
|
}
|
|
875
1141
|
};
|
|
876
|
-
let activeSlowPass = null;
|
|
877
|
-
let activeFastPass = null;
|
|
878
|
-
const pendingSlowPasses = [];
|
|
879
|
-
const pendingFastPasses = [];
|
|
880
1142
|
const resolveStoppedQueue = () => {
|
|
881
|
-
|
|
882
|
-
while (queue.length > 0)
|
|
883
|
-
queue.shift()?.resolve(0);
|
|
884
|
-
}
|
|
1143
|
+
passScheduler.stop();
|
|
885
1144
|
};
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
// refilling the realtime queue; otherwise a steady stream of fast work
|
|
893
|
-
// perpetually leapfrogs a receiver's bulk batch and its SQS poll loop
|
|
894
|
-
// cannot issue another receive.
|
|
895
|
-
if (activeSlowPass === null &&
|
|
896
|
-
activeFastPass === null &&
|
|
897
|
-
pendingSlowPasses.length > 0) {
|
|
898
|
-
const slow = pendingSlowPasses.shift();
|
|
899
|
-
if (slow) {
|
|
900
|
-
const current = startGuardedTask(slow.task, "slow");
|
|
901
|
-
void current.then(slow.resolve, slow.reject);
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
if (activeFastPass === null) {
|
|
905
|
-
const fast = pendingFastPasses.shift();
|
|
906
|
-
if (fast) {
|
|
907
|
-
const current = startGuardedTask(fast.task, "fast");
|
|
908
|
-
void current.then(fast.resolve, fast.reject);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
// A fast pass that acquired the operation lock normally (rather than
|
|
912
|
-
// bypassing an in-process slow holder) still owns the cross-process
|
|
913
|
-
// boundary. Slow work must queue behind it; only fast work is allowed to
|
|
914
|
-
// overlap an already-lock-owning slow pass.
|
|
915
|
-
if (activeSlowPass === null && activeFastPass === null) {
|
|
916
|
-
const slow = pendingSlowPasses.shift();
|
|
917
|
-
if (slow) {
|
|
918
|
-
const current = startGuardedTask(slow.task, "slow");
|
|
919
|
-
void current.then(slow.resolve, slow.reject);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
};
|
|
923
|
-
const startGuardedTask = (task, lane) => {
|
|
924
|
-
// Claim the lane before invoking `task`: a task can synchronously activate
|
|
925
|
-
// a watcher/scheduler callback before returning its promise. Without this
|
|
926
|
-
// placeholder that re-entrant callback sees an idle lane and starts a
|
|
927
|
-
// second lock contender.
|
|
928
|
-
const placeholder = Promise.resolve(0);
|
|
929
|
-
if (lane === "fast")
|
|
930
|
-
activeFastPass = placeholder;
|
|
931
|
-
else
|
|
932
|
-
activeSlowPass = placeholder;
|
|
933
|
-
let current;
|
|
934
|
-
try {
|
|
935
|
-
current = stopped ? placeholder : task();
|
|
936
|
-
}
|
|
937
|
-
catch (err) {
|
|
938
|
-
current = Promise.reject(err);
|
|
939
|
-
}
|
|
940
|
-
if (lane === "fast")
|
|
941
|
-
activeFastPass = current;
|
|
942
|
-
else
|
|
943
|
-
activeSlowPass = current;
|
|
944
|
-
void current
|
|
945
|
-
.finally(() => {
|
|
946
|
-
if (lane === "fast" && activeFastPass === current) {
|
|
947
|
-
activeFastPass = null;
|
|
948
|
-
drainQueuedPasses();
|
|
949
|
-
}
|
|
950
|
-
else if (lane === "slow" && activeSlowPass === current) {
|
|
951
|
-
activeSlowPass = null;
|
|
952
|
-
drainQueuedPasses();
|
|
953
|
-
}
|
|
954
|
-
})
|
|
955
|
-
.catch(() => undefined);
|
|
956
|
-
return current;
|
|
957
|
-
};
|
|
958
|
-
const runGuardedTask = (task, lane = "slow") => {
|
|
959
|
-
const active = lane === "fast"
|
|
960
|
-
? activeFastPass
|
|
961
|
-
: activeSlowPass ?? activeFastPass;
|
|
962
|
-
const queued = lane === "fast" ? pendingFastPasses : pendingSlowPasses;
|
|
963
|
-
if (active === null && queued.length === 0) {
|
|
964
|
-
return startGuardedTask(task, lane);
|
|
965
|
-
}
|
|
966
|
-
return new Promise((resolve, reject) => {
|
|
967
|
-
queued.push({ task, lane, resolve, reject });
|
|
968
|
-
drainQueuedPasses();
|
|
969
|
-
});
|
|
970
|
-
};
|
|
971
|
-
const runGuarded = (passArgvForRun, prepare, lane = "slow") => runGuardedTask(() => runPassWithLock(passArgvForRun, prepare, lane), lane);
|
|
1145
|
+
const runGuardedTask = (task, lane = "slow", workClass = "chore", allowFastRealtimeOvertake = false) =>
|
|
1146
|
+
// Lanes still choose the pass execution profile, but no longer decide
|
|
1147
|
+
// chore ownership. Realtime normally waits for a chore hand-off; the
|
|
1148
|
+
// watcher fast path explicitly opts into its established scoped exception.
|
|
1149
|
+
passScheduler.submit(task, workClass, lane, allowFastRealtimeOvertake);
|
|
1150
|
+
const runGuarded = (passArgvForRun, prepare, lane = "slow", workClass = "chore", allowFastRealtimeOvertake = false) => runGuardedTask((context) => runPassWithLock(passArgvForRun, prepare, lane, workClass, workClass === "chore" ? context.checkpoint : undefined), lane, workClass, allowFastRealtimeOvertake);
|
|
972
1151
|
/*
|
|
973
1152
|
* Area storage and the parallel coordinator default to AUTO. AUTO turns
|
|
974
1153
|
* them on after durable personal-vault cutover; explicit off is the kill
|
|
@@ -1160,6 +1339,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1160
1339
|
});
|
|
1161
1340
|
},
|
|
1162
1341
|
onCutOver: () => {
|
|
1342
|
+
releaseScopedJournalStore(PERSONAL_VAULT_JOURNAL_SLUG);
|
|
1163
1343
|
parallelCoordinatorEnabled = refreshCoordinatorEnabled();
|
|
1164
1344
|
if (parallelCoordinatorEnabled && coordinator === null) {
|
|
1165
1345
|
coordinator = createCoordinator();
|
|
@@ -1267,6 +1447,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1267
1447
|
let drainedUntrackedDeletes = 0;
|
|
1268
1448
|
const pendingWatcherPaths = new Map();
|
|
1269
1449
|
const pendingWatcherChanges = new Map();
|
|
1450
|
+
// A warm-up scan observes filesystem state over time. Keep an ingress
|
|
1451
|
+
// sequence so its older observations cannot overwrite a live watcher event
|
|
1452
|
+
// that arrived while the scan was yielding.
|
|
1453
|
+
const pendingWatcherChangeSequences = new Map();
|
|
1454
|
+
let nextPendingWatcherChangeSequence = 0;
|
|
1270
1455
|
let pendingWatcherOriginalBatch = null;
|
|
1271
1456
|
// Every coalesced watcher batch gets one durable in-process generation.
|
|
1272
1457
|
// A wake can cause the scheduler to revisit a drained batch, but an event
|
|
@@ -1290,11 +1475,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1290
1475
|
const paths = new Map();
|
|
1291
1476
|
const changes = new Map();
|
|
1292
1477
|
for (const [absolutePath, relativePath] of batch.paths.entries()) {
|
|
1293
|
-
const
|
|
1478
|
+
const change = batch.changes?.get(absolutePath);
|
|
1479
|
+
const route = routeChangeToTarget(relativePath, change?.kind);
|
|
1294
1480
|
if (!route || !isWatchRouteSelected(route, watchRouteSelection))
|
|
1295
1481
|
continue;
|
|
1296
1482
|
paths.set(absolutePath, relativePath);
|
|
1297
|
-
const change = batch.changes?.get(absolutePath);
|
|
1298
1483
|
if (change)
|
|
1299
1484
|
changes.set(absolutePath, change);
|
|
1300
1485
|
}
|
|
@@ -1302,7 +1487,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1302
1487
|
return null;
|
|
1303
1488
|
return { ...batch, paths, changes };
|
|
1304
1489
|
};
|
|
1305
|
-
const addPendingWatcherChange = (changedRelPath, batch) => {
|
|
1490
|
+
const addPendingWatcherChange = (changedRelPath, batch, options) => {
|
|
1306
1491
|
if (batch) {
|
|
1307
1492
|
const selectedBatch = selectWatchBatch(batch);
|
|
1308
1493
|
if (!selectedBatch)
|
|
@@ -1315,20 +1500,30 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1315
1500
|
return false;
|
|
1316
1501
|
}
|
|
1317
1502
|
}
|
|
1318
|
-
pendingWatcherBatchGeneration ??= ++nextWatcherBatchGeneration;
|
|
1319
1503
|
if (batch) {
|
|
1504
|
+
const entries = [...batch.paths.entries()].filter(([absolutePath]) => {
|
|
1505
|
+
const previousSequence = pendingWatcherChangeSequences.get(absolutePath);
|
|
1506
|
+
return previousSequence === undefined ||
|
|
1507
|
+
options?.preserveChangesAfter === undefined ||
|
|
1508
|
+
previousSequence <= options.preserveChangesAfter;
|
|
1509
|
+
});
|
|
1510
|
+
if (entries.length === 0)
|
|
1511
|
+
return false;
|
|
1512
|
+
pendingWatcherBatchGeneration ??= ++nextWatcherBatchGeneration;
|
|
1320
1513
|
pendingWatcherOriginalBatch =
|
|
1321
1514
|
pendingWatcherPaths.size === 0 &&
|
|
1322
1515
|
!pendingWatcherBareChange &&
|
|
1323
1516
|
!pendingWatcherOverflowed &&
|
|
1324
|
-
!batch.overflowed
|
|
1517
|
+
!batch.overflowed &&
|
|
1518
|
+
entries.length === batch.paths.size
|
|
1325
1519
|
? batch
|
|
1326
1520
|
: null;
|
|
1327
|
-
for (const [absolutePath, relativePath] of
|
|
1521
|
+
for (const [absolutePath, relativePath] of entries) {
|
|
1328
1522
|
pendingWatcherPaths.set(absolutePath, relativePath);
|
|
1329
1523
|
const change = batch.changes?.get(absolutePath);
|
|
1330
1524
|
if (change)
|
|
1331
1525
|
pendingWatcherChanges.set(absolutePath, change);
|
|
1526
|
+
pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
|
|
1332
1527
|
}
|
|
1333
1528
|
if (batch.overflowed) {
|
|
1334
1529
|
pendingWatcherOverflowed = true;
|
|
@@ -1345,6 +1540,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1345
1540
|
}
|
|
1346
1541
|
return true;
|
|
1347
1542
|
}
|
|
1543
|
+
pendingWatcherBatchGeneration ??= ++nextWatcherBatchGeneration;
|
|
1348
1544
|
if (changedRelPath) {
|
|
1349
1545
|
pendingWatcherOriginalBatch = null;
|
|
1350
1546
|
// Codec boundary: `changedRelPath` is a canonical vault key (the
|
|
@@ -1353,6 +1549,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1353
1549
|
// point at the impossible colon path on Windows.
|
|
1354
1550
|
const absolutePath = localPathForVaultKey(hqRoot, changedRelPath);
|
|
1355
1551
|
pendingWatcherPaths.set(absolutePath, changedRelPath);
|
|
1552
|
+
pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
|
|
1356
1553
|
// A bare path signal carries no delete intent; drop any change record
|
|
1357
1554
|
// a prior batch left for this path so the stale intent cannot leak
|
|
1358
1555
|
// into the coalesced batch (#227).
|
|
@@ -1394,6 +1591,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1394
1591
|
const generation = pendingWatcherBatchGeneration;
|
|
1395
1592
|
pendingWatcherPaths.clear();
|
|
1396
1593
|
pendingWatcherChanges.clear();
|
|
1594
|
+
pendingWatcherChangeSequences.clear();
|
|
1397
1595
|
pendingWatcherOriginalBatch = null;
|
|
1398
1596
|
pendingWatcherBareChange = false;
|
|
1399
1597
|
pendingWatcherWarmupCatchup = false;
|
|
@@ -1412,6 +1610,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1412
1610
|
pendingWatcherOriginalBatch = null;
|
|
1413
1611
|
for (const [absolutePath, relativePath] of paths.entries()) {
|
|
1414
1612
|
pendingWatcherPaths.set(absolutePath, relativePath);
|
|
1613
|
+
pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
|
|
1415
1614
|
}
|
|
1416
1615
|
};
|
|
1417
1616
|
/**
|
|
@@ -1492,8 +1691,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1492
1691
|
}
|
|
1493
1692
|
return null;
|
|
1494
1693
|
};
|
|
1495
|
-
const journalCoordinates = (relativePath) => {
|
|
1496
|
-
const route = routeChangeToTarget(relativePath);
|
|
1694
|
+
const journalCoordinates = (relativePath, kind) => {
|
|
1695
|
+
const route = routeChangeToTarget(relativePath, kind);
|
|
1497
1696
|
if (!route || !isWatchRouteSelected(route, watchRouteSelection))
|
|
1498
1697
|
return null;
|
|
1499
1698
|
if (route.kind === "personal") {
|
|
@@ -1528,7 +1727,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1528
1727
|
declaredSize >= 0) {
|
|
1529
1728
|
return declaredSize;
|
|
1530
1729
|
}
|
|
1531
|
-
const size = journalRows(
|
|
1730
|
+
const size = journalRows(readJournalScoped(coordinates.slug, [coordinates.key]))
|
|
1731
|
+
.get(coordinates.key)?.size;
|
|
1532
1732
|
return typeof size === "number" && Number.isSafeInteger(size) && size >= 0 ? size : null;
|
|
1533
1733
|
};
|
|
1534
1734
|
/**
|
|
@@ -1575,8 +1775,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1575
1775
|
return session.get(coordinates.key) === undefined;
|
|
1576
1776
|
};
|
|
1577
1777
|
const captureLocalDeleteSnapshots = (relativePath, kind) => {
|
|
1578
|
-
const coordinates = journalCoordinates(relativePath);
|
|
1579
|
-
const route = routeChangeToTarget(relativePath);
|
|
1778
|
+
const coordinates = journalCoordinates(relativePath, kind);
|
|
1779
|
+
const route = routeChangeToTarget(relativePath, kind);
|
|
1580
1780
|
if (!coordinates || !route)
|
|
1581
1781
|
return [];
|
|
1582
1782
|
// Desktop workspace Off toggles are authoritative before journal I/O.
|
|
@@ -1697,8 +1897,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1697
1897
|
* it scopes a push or pull for the affected batch.
|
|
1698
1898
|
*/
|
|
1699
1899
|
const captureQueuedLocalDeleteSnapshots = async (relativePath, kind, isCurrent) => {
|
|
1700
|
-
const coordinates = journalCoordinates(relativePath);
|
|
1701
|
-
const route = routeChangeToTarget(relativePath);
|
|
1900
|
+
const coordinates = journalCoordinates(relativePath, kind);
|
|
1901
|
+
const route = routeChangeToTarget(relativePath, kind);
|
|
1702
1902
|
if (!coordinates || !route)
|
|
1703
1903
|
return [];
|
|
1704
1904
|
if (route.kind === "company" &&
|
|
@@ -1792,8 +1992,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1792
1992
|
}
|
|
1793
1993
|
return snapshots;
|
|
1794
1994
|
};
|
|
1795
|
-
const warmupJournalPaths = ()
|
|
1796
|
-
const paths = [];
|
|
1995
|
+
const warmupJournalPaths = function* () {
|
|
1797
1996
|
const journals = listJournals();
|
|
1798
1997
|
const vaultJournalPresent = journals.some(({ slug }) => slug === PERSONAL_VAULT_JOURNAL_SLUG);
|
|
1799
1998
|
for (const { slug, journal } of journals) {
|
|
@@ -1826,10 +2025,9 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1826
2025
|
// ~12.8k of them per runner start after a transcript prune.
|
|
1827
2026
|
if (route.kind === "personal" && isPersonalVaultDeletionExcluded(key))
|
|
1828
2027
|
continue;
|
|
1829
|
-
|
|
2028
|
+
yield route.kind === "personal" ? key : `companies/${route.slug}/${key}`;
|
|
1830
2029
|
}
|
|
1831
2030
|
}
|
|
1832
|
-
return paths;
|
|
1833
2031
|
};
|
|
1834
2032
|
const publishedContentHashLookup = (relativePath) => {
|
|
1835
2033
|
const coordinates = journalCoordinates(relativePath);
|
|
@@ -1842,13 +2040,34 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1842
2040
|
return publishedContentHashFromJournalEntryBeforeSession(entry, serviceStartedAt);
|
|
1843
2041
|
};
|
|
1844
2042
|
const prepareWatcherChanges = (changes) => {
|
|
2043
|
+
const keysByJournal = new Map();
|
|
2044
|
+
const rememberKey = (slug, key) => {
|
|
2045
|
+
const keys = keysByJournal.get(slug) ?? new Set();
|
|
2046
|
+
keys.add(key);
|
|
2047
|
+
keysByJournal.set(slug, keys);
|
|
2048
|
+
};
|
|
2049
|
+
for (const [absolutePath, change] of changes.entries()) {
|
|
2050
|
+
if (change.kind === "unlink" || change.kind === "unlinkDir") {
|
|
2051
|
+
for (const snapshot of change.deleteSnapshots ?? []) {
|
|
2052
|
+
rememberKey(snapshot.journalSlug, snapshot.journalPath);
|
|
2053
|
+
}
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
const relativePath = path
|
|
2057
|
+
.relative(hqRoot, absolutePath)
|
|
2058
|
+
.split(path.sep)
|
|
2059
|
+
.join("/");
|
|
2060
|
+
const coordinates = journalCoordinates(relativePath);
|
|
2061
|
+
if (coordinates)
|
|
2062
|
+
rememberKey(coordinates.slug, coordinates.key);
|
|
2063
|
+
}
|
|
1845
2064
|
const journals = new Map();
|
|
1846
2065
|
const dirty = new Set();
|
|
1847
2066
|
const journalFor = (slug) => {
|
|
1848
2067
|
const loaded = journals.get(slug);
|
|
1849
2068
|
if (loaded)
|
|
1850
2069
|
return loaded;
|
|
1851
|
-
const journal =
|
|
2070
|
+
const journal = readJournalScoped(slug, [...(keysByJournal.get(slug) ?? [])]);
|
|
1852
2071
|
journals.set(slug, journal);
|
|
1853
2072
|
return journal;
|
|
1854
2073
|
};
|
|
@@ -1906,7 +2125,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1906
2125
|
const groupBatchByRoute = (batch) => {
|
|
1907
2126
|
const grouped = new Map();
|
|
1908
2127
|
for (const [absolutePath, relativePath] of batch.paths.entries()) {
|
|
1909
|
-
const
|
|
2128
|
+
const change = batch.changes?.get(absolutePath);
|
|
2129
|
+
const route = routeChangeToTarget(relativePath, change?.kind);
|
|
1910
2130
|
if (!route)
|
|
1911
2131
|
continue;
|
|
1912
2132
|
const key = routeKey(route);
|
|
@@ -1919,7 +2139,6 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1919
2139
|
deleteScopeRoots: [],
|
|
1920
2140
|
};
|
|
1921
2141
|
existing.paths.set(absolutePath, relativePath);
|
|
1922
|
-
const change = batch.changes?.get(absolutePath);
|
|
1923
2142
|
if (change)
|
|
1924
2143
|
existing.changes.set(absolutePath, change);
|
|
1925
2144
|
existing.relPaths = [...new Set([...existing.paths.values()])];
|
|
@@ -2003,7 +2222,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2003
2222
|
}
|
|
2004
2223
|
return false;
|
|
2005
2224
|
};
|
|
2006
|
-
const runScopedDrain = async (batch, watcherBatchGeneration, wakeOnPushFailure = false) => {
|
|
2225
|
+
const runScopedDrain = async (batch, watcherBatchGeneration, wakeOnPushFailure = false, allowFastRealtimeOvertake = false) => {
|
|
2007
2226
|
if (batch)
|
|
2008
2227
|
await resolveDeleteSnapshotCaptures(batch);
|
|
2009
2228
|
const grouped = batch && batch.paths.size > 0 ? groupBatchByRoute(batch) : null;
|
|
@@ -2032,11 +2251,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2032
2251
|
const scopedArgv = buildScopedPushArgv(group.route, admittedPaths, passArgv, group.deleteScopeRoots);
|
|
2033
2252
|
return runGuarded(scopedArgv, admittedChanges.size > 0
|
|
2034
2253
|
? () => prepareWatcherChanges(admittedChanges)
|
|
2035
|
-
: undefined, lane);
|
|
2254
|
+
: undefined, lane, "realtime", allowFastRealtimeOvertake);
|
|
2036
2255
|
})
|
|
2037
2256
|
: { result: await runGuarded(buildScopedPushArgv(group.route, group.relPaths, passArgv, group.deleteScopeRoots), group.changes.size > 0
|
|
2038
2257
|
? () => prepareWatcherChanges(group.changes)
|
|
2039
|
-
: undefined, lane), accepted: true, coordinatorRefusedPaths: [] };
|
|
2258
|
+
: undefined, lane, "realtime", allowFastRealtimeOvertake), accepted: true, coordinatorRefusedPaths: [] };
|
|
2040
2259
|
const result = execution.result;
|
|
2041
2260
|
if (execution.accepted && passExitCode(result) === 0) {
|
|
2042
2261
|
const refusedPaths = new Set(typeof result === "number"
|
|
@@ -2106,8 +2325,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2106
2325
|
? "fast"
|
|
2107
2326
|
: "slow";
|
|
2108
2327
|
const execution = parallelCoordinatorEnabled
|
|
2109
|
-
? await runRealtimePass("watcher", "pull", group.relPaths, lane, (slice) => runGuarded(buildTargetedPullArgv(group.route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, lane))
|
|
2110
|
-
: { result: await runGuarded(pullArgv, undefined, lane), accepted: true };
|
|
2328
|
+
? await runRealtimePass("watcher", "pull", group.relPaths, lane, (slice) => runGuarded(buildTargetedPullArgv(group.route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, lane, "realtime"))
|
|
2329
|
+
: { result: await runGuarded(pullArgv, undefined, lane, "realtime"), accepted: true };
|
|
2111
2330
|
const exit = execution.accepted ? passExitCode(execution.result) : 1;
|
|
2112
2331
|
if (exit !== 0 && worstExit === 0)
|
|
2113
2332
|
worstExit = exit;
|
|
@@ -2312,13 +2531,13 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2312
2531
|
return;
|
|
2313
2532
|
}
|
|
2314
2533
|
if (lanes?.fast &&
|
|
2315
|
-
|
|
2534
|
+
(passScheduler.hasActiveChore || passScheduler.hasActiveSlowRealtime)) {
|
|
2316
2535
|
const generation = ++nextWatcherBatchGeneration;
|
|
2317
2536
|
// Fast dispatch bypasses the pending-batch ingress, so preserve both
|
|
2318
2537
|
// of its contracts here: only selected routes may run, and V2 sees
|
|
2319
2538
|
// the same dirty signal as the compatibility drain.
|
|
2320
2539
|
realtimeState.scheduler?.signal("watcher");
|
|
2321
|
-
void runScopedDrain(lanes.fast, generation, true).then((exit) => {
|
|
2540
|
+
void runScopedDrain(lanes.fast, generation, true, true).then((exit) => {
|
|
2322
2541
|
if (exit === 0)
|
|
2323
2542
|
return;
|
|
2324
2543
|
restorePendingWatcherPaths(lanes.fast.paths, generation);
|
|
@@ -2373,29 +2592,36 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2373
2592
|
watcherRediscoveryRetries = 0;
|
|
2374
2593
|
process.stderr.write(`hq-sync-runner: event-push watcher active (watched-paths=${watchedPaths ?? "unknown"})\n`);
|
|
2375
2594
|
const startedAt = Date.now();
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2595
|
+
const liveChangeSequenceAtWarmupStart = nextPendingWatcherChangeSequence;
|
|
2596
|
+
void (async () => {
|
|
2597
|
+
try {
|
|
2598
|
+
const catchup = watcher?.collectWarmupCatchupAsync
|
|
2599
|
+
? await watcher.collectWarmupCatchupAsync(serviceStartedAt - WATCHER_WARMUP_SAFETY_MARGIN_MS, warmupJournalPaths())
|
|
2600
|
+
: watcher?.collectWarmupCatchup?.(serviceStartedAt - WATCHER_WARMUP_SAFETY_MARGIN_MS, warmupJournalPaths());
|
|
2601
|
+
if (!catchup || catchup.cancelled || stopped || watcher === null)
|
|
2602
|
+
return;
|
|
2603
|
+
const queued = addPendingWatcherChange(undefined, catchup.batch, {
|
|
2604
|
+
preserveChangesAfter: liveChangeSequenceAtWarmupStart,
|
|
2605
|
+
});
|
|
2606
|
+
pendingWatcherWarmupCatchup ||= queued;
|
|
2607
|
+
process.stderr.write(JSON.stringify({
|
|
2608
|
+
type: "watcher-warmup-catchup",
|
|
2609
|
+
reason: "watch-set-ready",
|
|
2610
|
+
pathsFound: catchup.batch.paths.size,
|
|
2611
|
+
scannedPaths: catchup.scannedPaths,
|
|
2612
|
+
elapsedMs: Date.now() - startedAt,
|
|
2613
|
+
queued,
|
|
2614
|
+
}) + "\n");
|
|
2615
|
+
if (queued)
|
|
2616
|
+
interruptWait();
|
|
2617
|
+
}
|
|
2618
|
+
catch (err) {
|
|
2619
|
+
process.stderr.write(`hq-sync-runner: watcher warm-up catch-up failed: ${describeError(err)}\n`);
|
|
2620
|
+
// A scan failure must not silently extend the warm-up blind spot.
|
|
2621
|
+
pendingWatcherWarmupCatchupFailed = true;
|
|
2391
2622
|
interruptWait();
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
process.stderr.write(`hq-sync-runner: watcher warm-up catch-up failed: ${describeError(err)}\n`);
|
|
2395
|
-
// A scan failure must not silently extend the warm-up blind spot.
|
|
2396
|
-
pendingWatcherWarmupCatchupFailed = true;
|
|
2397
|
-
interruptWait();
|
|
2398
|
-
}
|
|
2623
|
+
}
|
|
2624
|
+
})();
|
|
2399
2625
|
};
|
|
2400
2626
|
if (watcher.onReady)
|
|
2401
2627
|
watcher.onReady(reportWatcherReady);
|
|
@@ -2443,7 +2669,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2443
2669
|
// Submit every route in this lane before awaiting any result. In
|
|
2444
2670
|
// particular, a later route's fast receipt must never wait for an
|
|
2445
2671
|
// earlier route's slow pull to finish.
|
|
2446
|
-
const completed = await Promise.all([...eventsByRoute.values()].map(({ route, events }) => runRealtimePass("receiver", "pull", events.map((event) => event.relativePath), lane, (slice) => runGuarded(buildTargetedPullArgv(route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, lane))));
|
|
2672
|
+
const completed = await Promise.all([...eventsByRoute.values()].map(({ route, events }) => runRealtimePass("receiver", "pull", events.map((event) => event.relativePath), lane, (slice) => runGuarded(buildTargetedPullArgv(route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, lane, "realtime"))));
|
|
2447
2673
|
for (const execution of completed) {
|
|
2448
2674
|
if (!execution.accepted || passExitCode(execution.result) !== 0) {
|
|
2449
2675
|
throw new Error(`targeted pull failed with exit code ${passExitCode(execution.result)}`);
|
|
@@ -2497,7 +2723,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2497
2723
|
}
|
|
2498
2724
|
for (const { route, paths } of pathsByRoute.values()) {
|
|
2499
2725
|
const targetedArgv = buildTargetedPullArgv(route, passArgv, paths);
|
|
2500
|
-
const result = await runGuarded(targetedArgv, undefined, lane);
|
|
2726
|
+
const result = await runGuarded(targetedArgv, undefined, lane, "realtime");
|
|
2501
2727
|
if (passExitCode(result) !== 0) {
|
|
2502
2728
|
throw new Error(`targeted pull failed with exit code ${passExitCode(result)}`);
|
|
2503
2729
|
}
|
|
@@ -2545,14 +2771,14 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2545
2771
|
const execution = await runRealtimePass("wake", "pull", [], "slow", async () => runGuardedTask(() => runTaskWithLock(async () => {
|
|
2546
2772
|
await drain();
|
|
2547
2773
|
return 0;
|
|
2548
|
-
})));
|
|
2774
|
+
}, "realtime"), "slow", "realtime"));
|
|
2549
2775
|
outcome = execution.accepted ? execution.result : 1;
|
|
2550
2776
|
}
|
|
2551
2777
|
else {
|
|
2552
2778
|
outcome = await runGuardedTask(() => runTaskWithLock(async () => {
|
|
2553
2779
|
await drain();
|
|
2554
2780
|
return 0;
|
|
2555
|
-
}));
|
|
2781
|
+
}, "realtime"), "slow", "realtime");
|
|
2556
2782
|
}
|
|
2557
2783
|
}
|
|
2558
2784
|
catch (err) {
|
|
@@ -2748,8 +2974,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2748
2974
|
targetedJournalSessions: () => targetedJournalSessionCensusSizes().targetedJournalSessions,
|
|
2749
2975
|
targetedJournalRows: () => targetedJournalSessionCensusSizes().targetedJournalRows,
|
|
2750
2976
|
cachedAreaJournalMigrations: () => journalCacheCensusSizes().cachedAreaJournalMigrations,
|
|
2751
|
-
pendingSlowPasses: () =>
|
|
2752
|
-
pendingFastPasses: () =>
|
|
2977
|
+
pendingSlowPasses: () => passScheduler.pendingLaneCount("slow"),
|
|
2978
|
+
pendingFastPasses: () => passScheduler.pendingLaneCount("fast"),
|
|
2753
2979
|
},
|
|
2754
2980
|
eventSyncSizes: () => eventSync?.censusSizes?.(),
|
|
2755
2981
|
watcherSizes: () => watcher?.censusSizes?.(),
|
|
@@ -3103,11 +3329,14 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
3103
3329
|
quarantinedDeleteRoots.delete(root);
|
|
3104
3330
|
}
|
|
3105
3331
|
// Delete snapshot capture needs a fresh keyed view while an episode is
|
|
3106
|
-
// being assembled, but completed watch cycles do not.
|
|
3107
|
-
//
|
|
3108
|
-
//
|
|
3332
|
+
// being assembled, but completed watch cycles do not. Area-authoritative
|
|
3333
|
+
// journals are different: their bridge owns the packed aggregate that a
|
|
3334
|
+
// later watcher callback reuses. Dropping that bridge at every poll
|
|
3335
|
+
// makes the next scoped callback synchronously re-pack the whole vault.
|
|
3336
|
+
// Keep it for this runner's lifetime; layout/phase changes still close
|
|
3337
|
+
// it in openAreaMigrationIfAuthoritative(), and final shutdown releases
|
|
3338
|
+
// every bridge below.
|
|
3109
3339
|
releaseWatcherJournalStores();
|
|
3110
|
-
releaseCachedAreaJournalMigrations();
|
|
3111
3340
|
const exitCode = passExitCode(result);
|
|
3112
3341
|
if (exitCode === runtime.authRequiredPassExit) {
|
|
3113
3342
|
// The pass already emitted auth-error. Hand the sentinel to the
|
|
@@ -3142,8 +3371,19 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
3142
3371
|
: "") +
|
|
3143
3372
|
"; staying alive, retrying next poll\n");
|
|
3144
3373
|
}
|
|
3374
|
+
else if (exitCode === UNRESOLVED_COMPANY_EXIT) {
|
|
3375
|
+
// HQ-SYNC-COMPANY-ROUTE-UID: routed paths are derived from local
|
|
3376
|
+
// company directory names, which may not be memberships. The runner
|
|
3377
|
+
// has already emitted the resolution error; terminating here would
|
|
3378
|
+
// restart the outpost watcher every 60 seconds for each such route.
|
|
3379
|
+
// Keep the watcher alive while preserving the one-shot non-zero code.
|
|
3380
|
+
process.stderr.write("hq-sync-runner: watch pass skipped — unresolved company target; retrying next poll\n");
|
|
3381
|
+
}
|
|
3145
3382
|
else if (exitCode !== 0) {
|
|
3146
|
-
|
|
3383
|
+
// A scoped company-resolution or per-pass failure is isolated to that
|
|
3384
|
+
// pass. The error was emitted by the runner; terminating the daemon
|
|
3385
|
+
// recreates the startup blind window and strands newer watcher work.
|
|
3386
|
+
process.stderr.write(`hq-sync-runner: watch pass failed (exit=${exitCode}); staying alive, retrying next poll\n`);
|
|
3147
3387
|
}
|
|
3148
3388
|
const completedPostRepairPass = !enqueuedFullChore && (typeof result === "number"
|
|
3149
3389
|
? exitCode === 0
|
|
@@ -3247,6 +3487,10 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
3247
3487
|
releaseWatcherJournalStores();
|
|
3248
3488
|
releaseCachedAreaJournalMigrations();
|
|
3249
3489
|
await shutdown();
|
|
3490
|
+
// Coordinator shutdown waits for any active pass before the retained
|
|
3491
|
+
// projections are dropped. A shutdown must never evict a store that a
|
|
3492
|
+
// final scoped writer is still refreshing or committing through.
|
|
3493
|
+
disposeScopedJournalStores();
|
|
3250
3494
|
detachSignal?.();
|
|
3251
3495
|
}
|
|
3252
3496
|
}
|