@indigoai-us/hq-cloud 6.16.58-alpha.3 → 6.16.59
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 +0 -14
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +89 -57
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +313 -56
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts +3 -12
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +3 -65
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.js +1 -172
- package/dist/bin/sync-runner-planning.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +2 -51
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +165 -409
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +2 -19
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +5 -40
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.test.js +0 -47
- package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +13 -59
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +4 -48
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +229 -1499
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts +0 -19
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +0 -55
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +1 -87
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync-scope.test.js +0 -8
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +8 -12
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +152 -302
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +126 -256
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cognito-auth.d.ts +34 -1
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +92 -21
- package/dist/cognito-auth.js.map +1 -1
- package/dist/context.d.ts +5 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +92 -12
- package/dist/context.js.map +1 -1
- package/dist/context.test.js +77 -2
- package/dist/context.test.js.map +1 -1
- package/dist/ignore.d.ts +10 -0
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +47 -5
- package/dist/ignore.js.map +1 -1
- package/dist/ignore.test.js +30 -3
- package/dist/ignore.test.js.map +1 -1
- package/dist/journal-row-store.d.ts +5 -40
- package/dist/journal-row-store.d.ts.map +1 -1
- package/dist/journal-row-store.js +21 -240
- package/dist/journal-row-store.js.map +1 -1
- package/dist/journal-row-store.test.js +0 -54
- 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 +6 -11
- package/dist/journal-snapshot-v4.js.map +1 -1
- package/dist/journal-store.test.js +7 -324
- package/dist/journal-store.test.js.map +1 -1
- package/dist/journal.d.ts +16 -51
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +79 -303
- package/dist/journal.js.map +1 -1
- package/dist/lib/exit-codes.d.ts +0 -15
- package/dist/lib/exit-codes.d.ts.map +1 -1
- package/dist/lib/exit-codes.js +0 -15
- package/dist/lib/exit-codes.js.map +1 -1
- package/dist/lib/net-errors.d.ts +12 -0
- package/dist/lib/net-errors.d.ts.map +1 -1
- package/dist/lib/net-errors.js +35 -0
- package/dist/lib/net-errors.js.map +1 -1
- package/dist/lib/net-errors.test.js +19 -1
- package/dist/lib/net-errors.test.js.map +1 -1
- package/dist/machine-auth.test.js +6 -4
- package/dist/machine-auth.test.js.map +1 -1
- package/dist/machine-token-cache-isolation.test.d.ts +24 -0
- package/dist/machine-token-cache-isolation.test.d.ts.map +1 -0
- package/dist/machine-token-cache-isolation.test.js +241 -0
- package/dist/machine-token-cache-isolation.test.js.map +1 -0
- package/dist/manifest/build-manifest-resident-parity.test.js +3 -3
- package/dist/manifest/build-manifest-resident-parity.test.js.map +1 -1
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +3 -10
- package/dist/personal-vault.js.map +1 -1
- package/dist/personal-vault.test.js +0 -35
- package/dist/personal-vault.test.js.map +1 -1
- package/dist/remote-pull.d.ts.map +1 -1
- package/dist/remote-pull.js +2 -9
- package/dist/remote-pull.js.map +1 -1
- package/dist/remote-pull.test.js +2 -59
- package/dist/remote-pull.test.js.map +1 -1
- package/dist/s3.d.ts +0 -1
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +2 -20
- package/dist/s3.js.map +1 -1
- package/dist/sync/area-ledger.d.ts +3 -27
- package/dist/sync/area-ledger.d.ts.map +1 -1
- package/dist/sync/area-ledger.js +39 -149
- package/dist/sync/area-ledger.js.map +1 -1
- package/dist/sync/area-ledger.test.js +5 -192
- package/dist/sync/area-ledger.test.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.test.js +0 -29
- 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 +6 -14
- package/dist/sync/realtime-telemetry.js.map +1 -1
- package/dist/sync/realtime-telemetry.test.js +16 -136
- package/dist/sync/realtime-telemetry.test.js.map +1 -1
- package/dist/sync/state-store.d.ts +0 -6
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +2 -14
- package/dist/sync/state-store.js.map +1 -1
- package/dist/watcher.d.ts +0 -12
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +0 -131
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +0 -63
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -2
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts +0 -2
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts.map +0 -1
- package/dist/cli/onepass-pull-planner-regressions.test.js +0 -300
- package/dist/cli/onepass-pull-planner-regressions.test.js.map +0 -1
- package/dist/cli/onepass-pull-planner.test.d.ts +0 -2
- package/dist/cli/onepass-pull-planner.test.d.ts.map +0 -1
- package/dist/cli/onepass-pull-planner.test.js +0 -266
- package/dist/cli/onepass-pull-planner.test.js.map +0 -1
- package/dist/cli/streaming-pull-planner.bench.test.d.ts +0 -15
- package/dist/cli/streaming-pull-planner.bench.test.d.ts.map +0 -1
- package/dist/cli/streaming-pull-planner.bench.test.js +0 -273
- package/dist/cli/streaming-pull-planner.bench.test.js.map +0 -1
- package/dist/s3.list-cost.test.d.ts +0 -2
- package/dist/s3.list-cost.test.d.ts.map +0 -1
- package/dist/s3.list-cost.test.js +0 -51
- package/dist/s3.list-cost.test.js.map +0 -1
- package/dist/sync/remote-file-spool.d.ts +0 -16
- package/dist/sync/remote-file-spool.d.ts.map +0 -1
- package/dist/sync/remote-file-spool.js +0 -91
- package/dist/sync/remote-file-spool.js.map +0 -1
- package/dist/sync/remote-file-spool.test.d.ts +0 -2
- package/dist/sync/remote-file-spool.test.d.ts.map +0 -1
- package/dist/sync/remote-file-spool.test.js +0 -77
- package/dist/sync/remote-file-spool.test.js.map +0 -1
|
@@ -10,11 +10,11 @@ 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
|
|
13
|
+
import { PARTIAL_SYNC_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
|
-
import { clearLocalDeleteIntent, getStateDir, markLocalDeleteIntent, PERSONAL_VAULT_JOURNAL_SLUG, openJournalStoreSession, openJournalStoreSessionAsync, listJournalSlugs, listJournals, maintainAreaJournalSlice,
|
|
17
|
+
import { clearLocalDeleteIntent, getStateDir, markLocalDeleteIntent, PERSONAL_VAULT_JOURNAL_SLUG, openJournalStoreSession, openJournalStoreSessionAsync, listJournalSlugs, listJournals, maintainAreaJournalSlice, readJournal, releaseCachedAreaJournalMigrations, journalCacheCensusSizes, journalRows, readJournalScoped, writeJournal, } from "../journal.js";
|
|
18
18
|
import { isPersonalVaultDeletionExcluded } from "../personal-vault.js";
|
|
19
19
|
import { NoopPushReceiver, } from "../sync/push-receiver.js";
|
|
20
20
|
import { resolveEventSync, startEventSync as defaultStartEventSync, } from "../sync/event-sync.js";
|
|
@@ -30,219 +30,6 @@ import { InlineSyncWorkerAdapter, } from "../sync/sync-work.js";
|
|
|
30
30
|
import { buildRoutePullArgv, buildScopedPushArgv, buildScopedDrainPullArgv, buildTargetedPullArgv, isWatchRouteSelected, routeChangeToTarget, routeKey, } from "./sync-runner-watch-routes.js";
|
|
31
31
|
import { resolveSkipCompanies } from "./sync-runner-planning.js";
|
|
32
32
|
const FULL_RECONCILE_EVERY_TICKS = 10;
|
|
33
|
-
/** A deferred full chore must get the next admission after this long. */
|
|
34
|
-
export const CHORE_STARVATION_CAP_MS = 15 * 60 * 1000;
|
|
35
|
-
/**
|
|
36
|
-
* One durable-state owner at a time for a full chore. Realtime work never runs
|
|
37
|
-
* beside that chore: it explicitly awaits `checkpoint()` after a completed
|
|
38
|
-
* durable unit, then resumes once the hand-off is finished. A separately
|
|
39
|
-
* tracked, explicitly opted-in watcher fast pass may use the pre-existing
|
|
40
|
-
* scoped fast-over-slow exception, but never while a chore owns durable state.
|
|
41
|
-
*/
|
|
42
|
-
export class CooperativePassScheduler {
|
|
43
|
-
now;
|
|
44
|
-
starvationCapMs;
|
|
45
|
-
pending = [];
|
|
46
|
-
active = null;
|
|
47
|
-
// Scoped fast realtime work has the established in-process lock exception
|
|
48
|
-
// while a scoped slow realtime upload is in flight. Keep that slot separate
|
|
49
|
-
// from `active`: a chore still has exactly one durable-state owner.
|
|
50
|
-
activeFastRealtime = null;
|
|
51
|
-
stopped = false;
|
|
52
|
-
_handoffActive = false;
|
|
53
|
-
constructor(now, starvationCapMs = CHORE_STARVATION_CAP_MS) {
|
|
54
|
-
this.now = now;
|
|
55
|
-
this.starvationCapMs = starvationCapMs;
|
|
56
|
-
}
|
|
57
|
-
get handoffActive() {
|
|
58
|
-
return this._handoffActive;
|
|
59
|
-
}
|
|
60
|
-
get hasActiveChore() {
|
|
61
|
-
return this.active?.workClass === "chore";
|
|
62
|
-
}
|
|
63
|
-
get hasActiveSlowRealtime() {
|
|
64
|
-
return this.active?.workClass === "realtime" && this.active.lane === "slow";
|
|
65
|
-
}
|
|
66
|
-
pendingLaneCount(lane) {
|
|
67
|
-
return this.pending.filter((entry) => entry.lane === lane).length;
|
|
68
|
-
}
|
|
69
|
-
submit(task, workClass, lane = "slow", allowFastRealtimeOvertake = false) {
|
|
70
|
-
if (this.stopped)
|
|
71
|
-
return Promise.resolve(0);
|
|
72
|
-
const directEntry = {
|
|
73
|
-
task,
|
|
74
|
-
workClass,
|
|
75
|
-
lane,
|
|
76
|
-
allowFastRealtimeOvertake,
|
|
77
|
-
// An immediately admitted task cannot starve, so avoid sampling the
|
|
78
|
-
// cadence clock merely to stamp it. This preserves the loop's deadline
|
|
79
|
-
// accounting and keeps the timestamp meaningful only for real queues.
|
|
80
|
-
enqueuedAt: 0,
|
|
81
|
-
resolve: () => undefined,
|
|
82
|
-
reject: () => undefined,
|
|
83
|
-
};
|
|
84
|
-
if (this.canStartFastRealtime(directEntry)) {
|
|
85
|
-
return this.startFastRealtime(directEntry);
|
|
86
|
-
}
|
|
87
|
-
if (this.active === null &&
|
|
88
|
-
this.activeFastRealtime === null &&
|
|
89
|
-
this.pending.length === 0) {
|
|
90
|
-
return this.start(directEntry);
|
|
91
|
-
}
|
|
92
|
-
return new Promise((resolve, reject) => {
|
|
93
|
-
const entry = { ...directEntry, enqueuedAt: this.now(), resolve, reject };
|
|
94
|
-
this.pending.push(entry);
|
|
95
|
-
this.drain();
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
stop() {
|
|
99
|
-
this.stopped = true;
|
|
100
|
-
while (this.pending.length > 0)
|
|
101
|
-
this.pending.shift()?.resolve(0);
|
|
102
|
-
}
|
|
103
|
-
drain() {
|
|
104
|
-
if (this.stopped || this.pending.length === 0)
|
|
105
|
-
return;
|
|
106
|
-
if (this.active !== null) {
|
|
107
|
-
const fastRealtime = this.nextFastRealtime();
|
|
108
|
-
if (fastRealtime && this.canStartFastRealtime(fastRealtime)) {
|
|
109
|
-
this.pending.splice(this.pending.indexOf(fastRealtime), 1);
|
|
110
|
-
this.startFastRealtime(fastRealtime);
|
|
111
|
-
}
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (this.activeFastRealtime !== null)
|
|
115
|
-
return;
|
|
116
|
-
const next = this.takeNext();
|
|
117
|
-
if (next)
|
|
118
|
-
this.start(next);
|
|
119
|
-
}
|
|
120
|
-
takeNext() {
|
|
121
|
-
const now = this.now();
|
|
122
|
-
const starvedChore = this.pending.findIndex((entry) => entry.workClass === "chore" && now - entry.enqueuedAt >= this.starvationCapMs);
|
|
123
|
-
if (starvedChore >= 0)
|
|
124
|
-
return this.pending.splice(starvedChore, 1)[0];
|
|
125
|
-
// A receiver's bulk drain is still realtime work, but its slow lane is the
|
|
126
|
-
// progress boundary for that delivery. Preserve its established turn
|
|
127
|
-
// before refilling later fast realtime arrivals; this does not let a
|
|
128
|
-
// chore overtake any realtime work.
|
|
129
|
-
const realtime = this.nextRealtimeIndex();
|
|
130
|
-
if (realtime >= 0)
|
|
131
|
-
return this.pending.splice(realtime, 1)[0];
|
|
132
|
-
return this.pending.shift();
|
|
133
|
-
}
|
|
134
|
-
nextRealtimeIndex() {
|
|
135
|
-
const slowRealtime = this.pending.findIndex((entry) => entry.workClass === "realtime" && entry.lane === "slow");
|
|
136
|
-
return slowRealtime >= 0
|
|
137
|
-
? slowRealtime
|
|
138
|
-
: this.pending.findIndex((entry) => entry.workClass === "realtime");
|
|
139
|
-
}
|
|
140
|
-
nextFastRealtime() {
|
|
141
|
-
return this.pending.find((entry) => entry.workClass === "realtime" && entry.lane === "fast");
|
|
142
|
-
}
|
|
143
|
-
hasStarvedChore() {
|
|
144
|
-
const now = this.now();
|
|
145
|
-
return this.pending.some((entry) => entry.workClass === "chore" && now - entry.enqueuedAt >= this.starvationCapMs);
|
|
146
|
-
}
|
|
147
|
-
canStartFastRealtime(entry) {
|
|
148
|
-
return (!this._handoffActive &&
|
|
149
|
-
this.activeFastRealtime === null &&
|
|
150
|
-
entry.workClass === "realtime" &&
|
|
151
|
-
entry.lane === "fast" &&
|
|
152
|
-
entry.allowFastRealtimeOvertake &&
|
|
153
|
-
this.hasActiveSlowRealtime &&
|
|
154
|
-
!this.hasStarvedChore());
|
|
155
|
-
}
|
|
156
|
-
start(entry) {
|
|
157
|
-
this.active = entry;
|
|
158
|
-
let current;
|
|
159
|
-
try {
|
|
160
|
-
current = this.execute(entry);
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
current = Promise.reject(error);
|
|
164
|
-
}
|
|
165
|
-
void current.then((outcome) => {
|
|
166
|
-
entry.resolve(outcome);
|
|
167
|
-
this.complete(entry);
|
|
168
|
-
}, (error) => {
|
|
169
|
-
entry.reject(error);
|
|
170
|
-
this.complete(entry);
|
|
171
|
-
}).catch(() => undefined);
|
|
172
|
-
return current;
|
|
173
|
-
}
|
|
174
|
-
complete(entry) {
|
|
175
|
-
if (this.active === entry)
|
|
176
|
-
this.active = null;
|
|
177
|
-
this.drain();
|
|
178
|
-
}
|
|
179
|
-
startFastRealtime(entry) {
|
|
180
|
-
this.activeFastRealtime = entry;
|
|
181
|
-
let current;
|
|
182
|
-
try {
|
|
183
|
-
current = this.execute(entry);
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
current = Promise.reject(error);
|
|
187
|
-
}
|
|
188
|
-
void current.then((outcome) => {
|
|
189
|
-
entry.resolve(outcome);
|
|
190
|
-
this.completeFastRealtime(entry);
|
|
191
|
-
}, (error) => {
|
|
192
|
-
entry.reject(error);
|
|
193
|
-
this.completeFastRealtime(entry);
|
|
194
|
-
});
|
|
195
|
-
return current;
|
|
196
|
-
}
|
|
197
|
-
completeFastRealtime(entry) {
|
|
198
|
-
if (this.activeFastRealtime === entry)
|
|
199
|
-
this.activeFastRealtime = null;
|
|
200
|
-
this.drain();
|
|
201
|
-
}
|
|
202
|
-
execute(entry) {
|
|
203
|
-
return entry.task({ checkpoint: () => this.checkpoint(entry) });
|
|
204
|
-
}
|
|
205
|
-
async checkpoint(entry) {
|
|
206
|
-
if (this.stopped ||
|
|
207
|
-
entry.workClass !== "chore" ||
|
|
208
|
-
this.active !== entry ||
|
|
209
|
-
!this.pending.some((pending) => pending.workClass === "realtime")) {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
// `entry` is suspended at a caller-declared durable boundary. It remains
|
|
213
|
-
// the sole full-pass continuation and will resume after this bounded burst.
|
|
214
|
-
const deferredAt = this.now();
|
|
215
|
-
this.active = null;
|
|
216
|
-
this._handoffActive = true;
|
|
217
|
-
let handedOff = false;
|
|
218
|
-
try {
|
|
219
|
-
while (!this.stopped && this.now() - deferredAt < this.starvationCapMs) {
|
|
220
|
-
const realtimeIndex = this.nextRealtimeIndex();
|
|
221
|
-
if (realtimeIndex < 0)
|
|
222
|
-
break;
|
|
223
|
-
const realtime = this.pending.splice(realtimeIndex, 1)[0];
|
|
224
|
-
handedOff = true;
|
|
225
|
-
this.active = realtime;
|
|
226
|
-
try {
|
|
227
|
-
realtime.resolve(await this.execute(realtime));
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
realtime.reject(error);
|
|
231
|
-
}
|
|
232
|
-
finally {
|
|
233
|
-
if (this.active === realtime)
|
|
234
|
-
this.active = null;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
finally {
|
|
239
|
-
this._handoffActive = false;
|
|
240
|
-
if (!this.stopped)
|
|
241
|
-
this.active = entry;
|
|
242
|
-
}
|
|
243
|
-
return handedOff;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
33
|
const MAX_FULL_RECONCILE_REASONS = 7;
|
|
247
34
|
/**
|
|
248
35
|
* Area storage is AUTO by default and only applies after durable cutover.
|
|
@@ -722,7 +509,6 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
722
509
|
const idleHeartbeatIntervalMs = deps.idleHeartbeatIntervalMs ?? WATCH_IDLE_HEARTBEAT_INTERVAL_MS;
|
|
723
510
|
const monotonicNow = deps.monotonicNow ?? (() => performance.now());
|
|
724
511
|
const runPass = deps.runPass ?? runtime.runPassWithOperationLockAlreadyHeld;
|
|
725
|
-
const runPassSupportsCooperativeCheckpoint = deps.runPass === undefined || deps.runPassSupportsCooperativeCheckpoint === true;
|
|
726
512
|
const pollIdx = argv.indexOf("--poll-remote-ms");
|
|
727
513
|
// An explicit --poll-remote-ms pins a fixed interval (menubar/CLI still set
|
|
728
514
|
// one). When it is omitted, the interval is resolved fresh each cycle from
|
|
@@ -733,11 +519,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
733
519
|
const eventPush = argv.includes("--event-push");
|
|
734
520
|
const fullReconcileIntervalMs = resolveFullReconcileMs();
|
|
735
521
|
const companiesMode = argv.includes("--companies");
|
|
736
|
-
const
|
|
737
|
-
const companyMode = companyIdx >= 0;
|
|
738
|
-
// The tenant a single-company run owns. Carried into route selection so a
|
|
739
|
-
// watcher or receiver event under another company cannot queue a pass for it.
|
|
740
|
-
const companySlug = companyMode ? argv[companyIdx + 1] : undefined;
|
|
522
|
+
const companyMode = argv.includes("--company");
|
|
741
523
|
const personalMode = argv.includes("--personal");
|
|
742
524
|
// Keep this at the watcher ingress rather than importing the runner's
|
|
743
525
|
// resolver: sync-runner imports this loop, and a reverse value import would
|
|
@@ -746,7 +528,6 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
746
528
|
const watchRouteSelection = {
|
|
747
529
|
companies: companiesMode,
|
|
748
530
|
company: companyMode,
|
|
749
|
-
...(companySlug !== undefined ? { companySlug } : {}),
|
|
750
531
|
personal: personalMode,
|
|
751
532
|
skipPersonal: parsed.skipPersonal === true ||
|
|
752
533
|
skipPersonalEnv === "1" ||
|
|
@@ -891,27 +672,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
891
672
|
resolveNoOperationLockUsers = resolve;
|
|
892
673
|
});
|
|
893
674
|
};
|
|
894
|
-
const
|
|
895
|
-
const runPassWithLock = async (passArgvForRun, prepare, lane = "slow", workClass = "chore", checkpoint) => {
|
|
896
|
-
const reportedAppliedUpserts = new Set();
|
|
897
|
-
const recordDurableAppliedUpserts = (upserts) => {
|
|
898
|
-
for (const applied of upserts) {
|
|
899
|
-
const key = `${applied.relativePath}\u0000${applied.contentHash}`;
|
|
900
|
-
if (reportedAppliedUpserts.has(key))
|
|
901
|
-
continue;
|
|
902
|
-
reportedAppliedUpserts.add(key);
|
|
903
|
-
rememberAppliedUpsert(applied.relativePath, applied.contentHash);
|
|
904
|
-
}
|
|
905
|
-
};
|
|
906
|
-
const invokePass = () => {
|
|
907
|
-
if (!runPassSupportsCooperativeCheckpoint)
|
|
908
|
-
return runPass(passArgvForRun);
|
|
909
|
-
// This callback is emitted by the production runner after journal
|
|
910
|
-
// repair/seed and before fanout. It starts ingress early, but never
|
|
911
|
-
// admits a second durable writer: queued realtime still waits for the
|
|
912
|
-
// existing scheduler's cooperative hand-off checkpoint.
|
|
913
|
-
return runPass(passArgvForRun, checkpoint, () => activateEventPushSurfaces(), recordDurableAppliedUpserts);
|
|
914
|
-
};
|
|
675
|
+
const runPassWithLock = async (passArgvForRun, prepare, lane = "slow") => {
|
|
915
676
|
// Observe the heap at pass boundaries — a gated checkpoint immediately before
|
|
916
677
|
// admitting a pass and immediately after it returns. A pass that runs a long
|
|
917
678
|
// synchronous stretch starves the interval timer, so these boundary
|
|
@@ -923,7 +684,9 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
923
684
|
heapCheckpoint?.();
|
|
924
685
|
if (typeof outcome === "number")
|
|
925
686
|
return;
|
|
926
|
-
|
|
687
|
+
for (const applied of outcome.result?.appliedUpserts ?? []) {
|
|
688
|
+
rememberAppliedUpsert(applied.relativePath, applied.contentHash);
|
|
689
|
+
}
|
|
927
690
|
};
|
|
928
691
|
// Pre-admit boundary observation.
|
|
929
692
|
heapCheckpoint?.();
|
|
@@ -932,31 +695,13 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
932
695
|
// its keyed journal delta and per-object mutation are independent from the
|
|
933
696
|
// slow pass's bulk paths, and the slow pass already owns the root lock.
|
|
934
697
|
// Never bypass the lock when no slow pass is active.
|
|
935
|
-
if (
|
|
936
|
-
// A full chore is paused at a caller-declared durable checkpoint. Keep
|
|
937
|
-
// the root lock held by this process, but lend it to exactly one
|
|
938
|
-
// realtime pass; the suspended chore cannot mutate until this returns.
|
|
939
|
-
workClass === "realtime" &&
|
|
940
|
-
passScheduler.handoffActive &&
|
|
941
|
-
processOwnsOperationLock) {
|
|
942
|
-
acquireOperationLockUser();
|
|
943
|
-
try {
|
|
944
|
-
prepare?.();
|
|
945
|
-
const outcome = await invokePass();
|
|
946
|
-
recordAppliedUpserts(outcome);
|
|
947
|
-
return outcome;
|
|
948
|
-
}
|
|
949
|
-
finally {
|
|
950
|
-
releaseOperationLockUser();
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
698
|
if (lane === "fast" &&
|
|
954
699
|
processOwnsOperationLock &&
|
|
955
700
|
coordinatorWholeMutationChores === 0) {
|
|
956
701
|
acquireOperationLockUser();
|
|
957
702
|
try {
|
|
958
703
|
prepare?.();
|
|
959
|
-
const outcome = await
|
|
704
|
+
const outcome = await runPass(passArgvForRun);
|
|
960
705
|
recordAppliedUpserts(outcome);
|
|
961
706
|
return outcome;
|
|
962
707
|
}
|
|
@@ -976,7 +721,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
976
721
|
acquireOperationLockUser();
|
|
977
722
|
}
|
|
978
723
|
prepare?.();
|
|
979
|
-
const outcome = await
|
|
724
|
+
const outcome = await runPass(passArgvForRun);
|
|
980
725
|
recordAppliedUpserts(outcome);
|
|
981
726
|
return outcome;
|
|
982
727
|
}
|
|
@@ -1010,7 +755,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1010
755
|
}
|
|
1011
756
|
try {
|
|
1012
757
|
prepare?.();
|
|
1013
|
-
const outcome = await
|
|
758
|
+
const outcome = await runPass(passArgvForRun);
|
|
1014
759
|
recordAppliedUpserts(outcome);
|
|
1015
760
|
return outcome;
|
|
1016
761
|
}
|
|
@@ -1033,18 +778,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1033
778
|
throw err;
|
|
1034
779
|
}
|
|
1035
780
|
};
|
|
1036
|
-
const runTaskWithLock = async (task
|
|
1037
|
-
if (workClass === "realtime" &&
|
|
1038
|
-
passScheduler.handoffActive &&
|
|
1039
|
-
processOwnsOperationLock) {
|
|
1040
|
-
acquireOperationLockUser();
|
|
1041
|
-
try {
|
|
1042
|
-
return await task();
|
|
1043
|
-
}
|
|
1044
|
-
finally {
|
|
1045
|
-
releaseOperationLockUser();
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
781
|
+
const runTaskWithLock = async (task) => {
|
|
1048
782
|
try {
|
|
1049
783
|
const handle = acquireOperationLock(hqRoot, "sync", {
|
|
1050
784
|
timeoutSec: 0,
|
|
@@ -1138,15 +872,102 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1138
872
|
heapCheckpoint?.();
|
|
1139
873
|
}
|
|
1140
874
|
};
|
|
875
|
+
let activeSlowPass = null;
|
|
876
|
+
let activeFastPass = null;
|
|
877
|
+
const pendingSlowPasses = [];
|
|
878
|
+
const pendingFastPasses = [];
|
|
1141
879
|
const resolveStoppedQueue = () => {
|
|
1142
|
-
|
|
880
|
+
for (const queue of [pendingFastPasses, pendingSlowPasses]) {
|
|
881
|
+
while (queue.length > 0)
|
|
882
|
+
queue.shift()?.resolve(0);
|
|
883
|
+
}
|
|
1143
884
|
};
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
885
|
+
const drainQueuedPasses = () => {
|
|
886
|
+
if (stopped) {
|
|
887
|
+
resolveStoppedQueue();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
// A queued slow pass is the bulk-drain progress boundary. Admit it before
|
|
891
|
+
// refilling the realtime queue; otherwise a steady stream of fast work
|
|
892
|
+
// perpetually leapfrogs a receiver's bulk batch and its SQS poll loop
|
|
893
|
+
// cannot issue another receive.
|
|
894
|
+
if (activeSlowPass === null &&
|
|
895
|
+
activeFastPass === null &&
|
|
896
|
+
pendingSlowPasses.length > 0) {
|
|
897
|
+
const slow = pendingSlowPasses.shift();
|
|
898
|
+
if (slow) {
|
|
899
|
+
const current = startGuardedTask(slow.task, "slow");
|
|
900
|
+
void current.then(slow.resolve, slow.reject);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (activeFastPass === null) {
|
|
904
|
+
const fast = pendingFastPasses.shift();
|
|
905
|
+
if (fast) {
|
|
906
|
+
const current = startGuardedTask(fast.task, "fast");
|
|
907
|
+
void current.then(fast.resolve, fast.reject);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
// A fast pass that acquired the operation lock normally (rather than
|
|
911
|
+
// bypassing an in-process slow holder) still owns the cross-process
|
|
912
|
+
// boundary. Slow work must queue behind it; only fast work is allowed to
|
|
913
|
+
// overlap an already-lock-owning slow pass.
|
|
914
|
+
if (activeSlowPass === null && activeFastPass === null) {
|
|
915
|
+
const slow = pendingSlowPasses.shift();
|
|
916
|
+
if (slow) {
|
|
917
|
+
const current = startGuardedTask(slow.task, "slow");
|
|
918
|
+
void current.then(slow.resolve, slow.reject);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
const startGuardedTask = (task, lane) => {
|
|
923
|
+
// Claim the lane before invoking `task`: a task can synchronously activate
|
|
924
|
+
// a watcher/scheduler callback before returning its promise. Without this
|
|
925
|
+
// placeholder that re-entrant callback sees an idle lane and starts a
|
|
926
|
+
// second lock contender.
|
|
927
|
+
const placeholder = Promise.resolve(0);
|
|
928
|
+
if (lane === "fast")
|
|
929
|
+
activeFastPass = placeholder;
|
|
930
|
+
else
|
|
931
|
+
activeSlowPass = placeholder;
|
|
932
|
+
let current;
|
|
933
|
+
try {
|
|
934
|
+
current = stopped ? placeholder : task();
|
|
935
|
+
}
|
|
936
|
+
catch (err) {
|
|
937
|
+
current = Promise.reject(err);
|
|
938
|
+
}
|
|
939
|
+
if (lane === "fast")
|
|
940
|
+
activeFastPass = current;
|
|
941
|
+
else
|
|
942
|
+
activeSlowPass = current;
|
|
943
|
+
void current
|
|
944
|
+
.finally(() => {
|
|
945
|
+
if (lane === "fast" && activeFastPass === current) {
|
|
946
|
+
activeFastPass = null;
|
|
947
|
+
drainQueuedPasses();
|
|
948
|
+
}
|
|
949
|
+
else if (lane === "slow" && activeSlowPass === current) {
|
|
950
|
+
activeSlowPass = null;
|
|
951
|
+
drainQueuedPasses();
|
|
952
|
+
}
|
|
953
|
+
})
|
|
954
|
+
.catch(() => undefined);
|
|
955
|
+
return current;
|
|
956
|
+
};
|
|
957
|
+
const runGuardedTask = (task, lane = "slow") => {
|
|
958
|
+
const active = lane === "fast"
|
|
959
|
+
? activeFastPass
|
|
960
|
+
: activeSlowPass ?? activeFastPass;
|
|
961
|
+
const queued = lane === "fast" ? pendingFastPasses : pendingSlowPasses;
|
|
962
|
+
if (active === null && queued.length === 0) {
|
|
963
|
+
return startGuardedTask(task, lane);
|
|
964
|
+
}
|
|
965
|
+
return new Promise((resolve, reject) => {
|
|
966
|
+
queued.push({ task, lane, resolve, reject });
|
|
967
|
+
drainQueuedPasses();
|
|
968
|
+
});
|
|
969
|
+
};
|
|
970
|
+
const runGuarded = (passArgvForRun, prepare, lane = "slow") => runGuardedTask(() => runPassWithLock(passArgvForRun, prepare, lane), lane);
|
|
1150
971
|
/*
|
|
1151
972
|
* Area storage and the parallel coordinator default to AUTO. AUTO turns
|
|
1152
973
|
* them on after durable personal-vault cutover; explicit off is the kill
|
|
@@ -1338,7 +1159,6 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1338
1159
|
});
|
|
1339
1160
|
},
|
|
1340
1161
|
onCutOver: () => {
|
|
1341
|
-
releaseScopedJournalStore(PERSONAL_VAULT_JOURNAL_SLUG);
|
|
1342
1162
|
parallelCoordinatorEnabled = refreshCoordinatorEnabled();
|
|
1343
1163
|
if (parallelCoordinatorEnabled && coordinator === null) {
|
|
1344
1164
|
coordinator = createCoordinator();
|
|
@@ -1425,11 +1245,6 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1425
1245
|
let drainedUntrackedDeletes = 0;
|
|
1426
1246
|
const pendingWatcherPaths = new Map();
|
|
1427
1247
|
const pendingWatcherChanges = new Map();
|
|
1428
|
-
// A warm-up scan observes filesystem state over time. Keep an ingress
|
|
1429
|
-
// sequence so its older observations cannot overwrite a live watcher event
|
|
1430
|
-
// that arrived while the scan was yielding.
|
|
1431
|
-
const pendingWatcherChangeSequences = new Map();
|
|
1432
|
-
let nextPendingWatcherChangeSequence = 0;
|
|
1433
1248
|
let pendingWatcherOriginalBatch = null;
|
|
1434
1249
|
// Every coalesced watcher batch gets one durable in-process generation.
|
|
1435
1250
|
// A wake can cause the scheduler to revisit a drained batch, but an event
|
|
@@ -1453,11 +1268,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1453
1268
|
const paths = new Map();
|
|
1454
1269
|
const changes = new Map();
|
|
1455
1270
|
for (const [absolutePath, relativePath] of batch.paths.entries()) {
|
|
1456
|
-
const
|
|
1457
|
-
const route = routeChangeToTarget(relativePath, change?.kind);
|
|
1271
|
+
const route = routeChangeToTarget(relativePath);
|
|
1458
1272
|
if (!route || !isWatchRouteSelected(route, watchRouteSelection))
|
|
1459
1273
|
continue;
|
|
1460
1274
|
paths.set(absolutePath, relativePath);
|
|
1275
|
+
const change = batch.changes?.get(absolutePath);
|
|
1461
1276
|
if (change)
|
|
1462
1277
|
changes.set(absolutePath, change);
|
|
1463
1278
|
}
|
|
@@ -1465,7 +1280,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1465
1280
|
return null;
|
|
1466
1281
|
return { ...batch, paths, changes };
|
|
1467
1282
|
};
|
|
1468
|
-
const addPendingWatcherChange = (changedRelPath, batch
|
|
1283
|
+
const addPendingWatcherChange = (changedRelPath, batch) => {
|
|
1469
1284
|
if (batch) {
|
|
1470
1285
|
const selectedBatch = selectWatchBatch(batch);
|
|
1471
1286
|
if (!selectedBatch)
|
|
@@ -1478,30 +1293,20 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1478
1293
|
return false;
|
|
1479
1294
|
}
|
|
1480
1295
|
}
|
|
1296
|
+
pendingWatcherBatchGeneration ??= ++nextWatcherBatchGeneration;
|
|
1481
1297
|
if (batch) {
|
|
1482
|
-
const entries = [...batch.paths.entries()].filter(([absolutePath]) => {
|
|
1483
|
-
const previousSequence = pendingWatcherChangeSequences.get(absolutePath);
|
|
1484
|
-
return previousSequence === undefined ||
|
|
1485
|
-
options?.preserveChangesAfter === undefined ||
|
|
1486
|
-
previousSequence <= options.preserveChangesAfter;
|
|
1487
|
-
});
|
|
1488
|
-
if (entries.length === 0)
|
|
1489
|
-
return false;
|
|
1490
|
-
pendingWatcherBatchGeneration ??= ++nextWatcherBatchGeneration;
|
|
1491
1298
|
pendingWatcherOriginalBatch =
|
|
1492
1299
|
pendingWatcherPaths.size === 0 &&
|
|
1493
1300
|
!pendingWatcherBareChange &&
|
|
1494
1301
|
!pendingWatcherOverflowed &&
|
|
1495
|
-
!batch.overflowed
|
|
1496
|
-
entries.length === batch.paths.size
|
|
1302
|
+
!batch.overflowed
|
|
1497
1303
|
? batch
|
|
1498
1304
|
: null;
|
|
1499
|
-
for (const [absolutePath, relativePath] of entries) {
|
|
1305
|
+
for (const [absolutePath, relativePath] of batch.paths.entries()) {
|
|
1500
1306
|
pendingWatcherPaths.set(absolutePath, relativePath);
|
|
1501
1307
|
const change = batch.changes?.get(absolutePath);
|
|
1502
1308
|
if (change)
|
|
1503
1309
|
pendingWatcherChanges.set(absolutePath, change);
|
|
1504
|
-
pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
|
|
1505
1310
|
}
|
|
1506
1311
|
if (batch.overflowed) {
|
|
1507
1312
|
pendingWatcherOverflowed = true;
|
|
@@ -1518,7 +1323,6 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1518
1323
|
}
|
|
1519
1324
|
return true;
|
|
1520
1325
|
}
|
|
1521
|
-
pendingWatcherBatchGeneration ??= ++nextWatcherBatchGeneration;
|
|
1522
1326
|
if (changedRelPath) {
|
|
1523
1327
|
pendingWatcherOriginalBatch = null;
|
|
1524
1328
|
// Codec boundary: `changedRelPath` is a canonical vault key (the
|
|
@@ -1527,7 +1331,6 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1527
1331
|
// point at the impossible colon path on Windows.
|
|
1528
1332
|
const absolutePath = localPathForVaultKey(hqRoot, changedRelPath);
|
|
1529
1333
|
pendingWatcherPaths.set(absolutePath, changedRelPath);
|
|
1530
|
-
pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
|
|
1531
1334
|
// A bare path signal carries no delete intent; drop any change record
|
|
1532
1335
|
// a prior batch left for this path so the stale intent cannot leak
|
|
1533
1336
|
// into the coalesced batch (#227).
|
|
@@ -1569,7 +1372,6 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1569
1372
|
const generation = pendingWatcherBatchGeneration;
|
|
1570
1373
|
pendingWatcherPaths.clear();
|
|
1571
1374
|
pendingWatcherChanges.clear();
|
|
1572
|
-
pendingWatcherChangeSequences.clear();
|
|
1573
1375
|
pendingWatcherOriginalBatch = null;
|
|
1574
1376
|
pendingWatcherBareChange = false;
|
|
1575
1377
|
pendingWatcherWarmupCatchup = false;
|
|
@@ -1588,7 +1390,6 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1588
1390
|
pendingWatcherOriginalBatch = null;
|
|
1589
1391
|
for (const [absolutePath, relativePath] of paths.entries()) {
|
|
1590
1392
|
pendingWatcherPaths.set(absolutePath, relativePath);
|
|
1591
|
-
pendingWatcherChangeSequences.set(absolutePath, ++nextPendingWatcherChangeSequence);
|
|
1592
1393
|
}
|
|
1593
1394
|
};
|
|
1594
1395
|
/**
|
|
@@ -1669,8 +1470,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1669
1470
|
}
|
|
1670
1471
|
return null;
|
|
1671
1472
|
};
|
|
1672
|
-
const journalCoordinates = (relativePath
|
|
1673
|
-
const route = routeChangeToTarget(relativePath
|
|
1473
|
+
const journalCoordinates = (relativePath) => {
|
|
1474
|
+
const route = routeChangeToTarget(relativePath);
|
|
1674
1475
|
if (!route || !isWatchRouteSelected(route, watchRouteSelection))
|
|
1675
1476
|
return null;
|
|
1676
1477
|
if (route.kind === "personal") {
|
|
@@ -1705,8 +1506,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1705
1506
|
declaredSize >= 0) {
|
|
1706
1507
|
return declaredSize;
|
|
1707
1508
|
}
|
|
1708
|
-
const size = journalRows(
|
|
1709
|
-
.get(coordinates.key)?.size;
|
|
1509
|
+
const size = journalRows(readJournal(coordinates.slug)).get(coordinates.key)?.size;
|
|
1710
1510
|
return typeof size === "number" && Number.isSafeInteger(size) && size >= 0 ? size : null;
|
|
1711
1511
|
};
|
|
1712
1512
|
/**
|
|
@@ -1753,8 +1553,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1753
1553
|
return session.get(coordinates.key) === undefined;
|
|
1754
1554
|
};
|
|
1755
1555
|
const captureLocalDeleteSnapshots = (relativePath, kind) => {
|
|
1756
|
-
const coordinates = journalCoordinates(relativePath
|
|
1757
|
-
const route = routeChangeToTarget(relativePath
|
|
1556
|
+
const coordinates = journalCoordinates(relativePath);
|
|
1557
|
+
const route = routeChangeToTarget(relativePath);
|
|
1758
1558
|
if (!coordinates || !route)
|
|
1759
1559
|
return [];
|
|
1760
1560
|
// Desktop workspace Off toggles are authoritative before journal I/O.
|
|
@@ -1875,8 +1675,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1875
1675
|
* it scopes a push or pull for the affected batch.
|
|
1876
1676
|
*/
|
|
1877
1677
|
const captureQueuedLocalDeleteSnapshots = async (relativePath, kind, isCurrent) => {
|
|
1878
|
-
const coordinates = journalCoordinates(relativePath
|
|
1879
|
-
const route = routeChangeToTarget(relativePath
|
|
1678
|
+
const coordinates = journalCoordinates(relativePath);
|
|
1679
|
+
const route = routeChangeToTarget(relativePath);
|
|
1880
1680
|
if (!coordinates || !route)
|
|
1881
1681
|
return [];
|
|
1882
1682
|
if (route.kind === "company" &&
|
|
@@ -1970,7 +1770,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
1970
1770
|
}
|
|
1971
1771
|
return snapshots;
|
|
1972
1772
|
};
|
|
1973
|
-
const warmupJournalPaths =
|
|
1773
|
+
const warmupJournalPaths = () => {
|
|
1774
|
+
const paths = [];
|
|
1974
1775
|
const journals = listJournals();
|
|
1975
1776
|
const vaultJournalPresent = journals.some(({ slug }) => slug === PERSONAL_VAULT_JOURNAL_SLUG);
|
|
1976
1777
|
for (const { slug, journal } of journals) {
|
|
@@ -2003,9 +1804,10 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2003
1804
|
// ~12.8k of them per runner start after a transcript prune.
|
|
2004
1805
|
if (route.kind === "personal" && isPersonalVaultDeletionExcluded(key))
|
|
2005
1806
|
continue;
|
|
2006
|
-
|
|
1807
|
+
paths.push(route.kind === "personal" ? key : `companies/${route.slug}/${key}`);
|
|
2007
1808
|
}
|
|
2008
1809
|
}
|
|
1810
|
+
return paths;
|
|
2009
1811
|
};
|
|
2010
1812
|
const publishedContentHashLookup = (relativePath) => {
|
|
2011
1813
|
const coordinates = journalCoordinates(relativePath);
|
|
@@ -2018,34 +1820,13 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2018
1820
|
return publishedContentHashFromJournalEntryBeforeSession(entry, serviceStartedAt);
|
|
2019
1821
|
};
|
|
2020
1822
|
const prepareWatcherChanges = (changes) => {
|
|
2021
|
-
const keysByJournal = new Map();
|
|
2022
|
-
const rememberKey = (slug, key) => {
|
|
2023
|
-
const keys = keysByJournal.get(slug) ?? new Set();
|
|
2024
|
-
keys.add(key);
|
|
2025
|
-
keysByJournal.set(slug, keys);
|
|
2026
|
-
};
|
|
2027
|
-
for (const [absolutePath, change] of changes.entries()) {
|
|
2028
|
-
if (change.kind === "unlink" || change.kind === "unlinkDir") {
|
|
2029
|
-
for (const snapshot of change.deleteSnapshots ?? []) {
|
|
2030
|
-
rememberKey(snapshot.journalSlug, snapshot.journalPath);
|
|
2031
|
-
}
|
|
2032
|
-
continue;
|
|
2033
|
-
}
|
|
2034
|
-
const relativePath = path
|
|
2035
|
-
.relative(hqRoot, absolutePath)
|
|
2036
|
-
.split(path.sep)
|
|
2037
|
-
.join("/");
|
|
2038
|
-
const coordinates = journalCoordinates(relativePath);
|
|
2039
|
-
if (coordinates)
|
|
2040
|
-
rememberKey(coordinates.slug, coordinates.key);
|
|
2041
|
-
}
|
|
2042
1823
|
const journals = new Map();
|
|
2043
1824
|
const dirty = new Set();
|
|
2044
1825
|
const journalFor = (slug) => {
|
|
2045
1826
|
const loaded = journals.get(slug);
|
|
2046
1827
|
if (loaded)
|
|
2047
1828
|
return loaded;
|
|
2048
|
-
const journal =
|
|
1829
|
+
const journal = readJournal(slug);
|
|
2049
1830
|
journals.set(slug, journal);
|
|
2050
1831
|
return journal;
|
|
2051
1832
|
};
|
|
@@ -2103,8 +1884,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2103
1884
|
const groupBatchByRoute = (batch) => {
|
|
2104
1885
|
const grouped = new Map();
|
|
2105
1886
|
for (const [absolutePath, relativePath] of batch.paths.entries()) {
|
|
2106
|
-
const
|
|
2107
|
-
const route = routeChangeToTarget(relativePath, change?.kind);
|
|
1887
|
+
const route = routeChangeToTarget(relativePath);
|
|
2108
1888
|
if (!route)
|
|
2109
1889
|
continue;
|
|
2110
1890
|
const key = routeKey(route);
|
|
@@ -2117,6 +1897,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2117
1897
|
deleteScopeRoots: [],
|
|
2118
1898
|
};
|
|
2119
1899
|
existing.paths.set(absolutePath, relativePath);
|
|
1900
|
+
const change = batch.changes?.get(absolutePath);
|
|
2120
1901
|
if (change)
|
|
2121
1902
|
existing.changes.set(absolutePath, change);
|
|
2122
1903
|
existing.relPaths = [...new Set([...existing.paths.values()])];
|
|
@@ -2200,7 +1981,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2200
1981
|
}
|
|
2201
1982
|
return false;
|
|
2202
1983
|
};
|
|
2203
|
-
const runScopedDrain = async (batch, watcherBatchGeneration, wakeOnPushFailure = false
|
|
1984
|
+
const runScopedDrain = async (batch, watcherBatchGeneration, wakeOnPushFailure = false) => {
|
|
2204
1985
|
if (batch)
|
|
2205
1986
|
await resolveDeleteSnapshotCaptures(batch);
|
|
2206
1987
|
const grouped = batch && batch.paths.size > 0 ? groupBatchByRoute(batch) : null;
|
|
@@ -2229,11 +2010,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2229
2010
|
const scopedArgv = buildScopedPushArgv(group.route, admittedPaths, passArgv, group.deleteScopeRoots);
|
|
2230
2011
|
return runGuarded(scopedArgv, admittedChanges.size > 0
|
|
2231
2012
|
? () => prepareWatcherChanges(admittedChanges)
|
|
2232
|
-
: undefined, lane
|
|
2013
|
+
: undefined, lane);
|
|
2233
2014
|
})
|
|
2234
2015
|
: { result: await runGuarded(buildScopedPushArgv(group.route, group.relPaths, passArgv, group.deleteScopeRoots), group.changes.size > 0
|
|
2235
2016
|
? () => prepareWatcherChanges(group.changes)
|
|
2236
|
-
: undefined, lane
|
|
2017
|
+
: undefined, lane), accepted: true, coordinatorRefusedPaths: [] };
|
|
2237
2018
|
const result = execution.result;
|
|
2238
2019
|
if (execution.accepted && passExitCode(result) === 0) {
|
|
2239
2020
|
const refusedPaths = new Set(typeof result === "number"
|
|
@@ -2303,8 +2084,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2303
2084
|
? "fast"
|
|
2304
2085
|
: "slow";
|
|
2305
2086
|
const execution = parallelCoordinatorEnabled
|
|
2306
|
-
? await runRealtimePass("watcher", "pull", group.relPaths, lane, (slice) => runGuarded(buildTargetedPullArgv(group.route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, lane
|
|
2307
|
-
: { result: await runGuarded(pullArgv, undefined, lane
|
|
2087
|
+
? await runRealtimePass("watcher", "pull", group.relPaths, lane, (slice) => runGuarded(buildTargetedPullArgv(group.route, passArgv, slice.paths.map((path) => path.canonicalPath)), undefined, lane))
|
|
2088
|
+
: { result: await runGuarded(pullArgv, undefined, lane), accepted: true };
|
|
2308
2089
|
const exit = execution.accepted ? passExitCode(execution.result) : 1;
|
|
2309
2090
|
if (exit !== 0 && worstExit === 0)
|
|
2310
2091
|
worstExit = exit;
|
|
@@ -2509,13 +2290,13 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2509
2290
|
return;
|
|
2510
2291
|
}
|
|
2511
2292
|
if (lanes?.fast &&
|
|
2512
|
-
|
|
2293
|
+
activeSlowPass !== null) {
|
|
2513
2294
|
const generation = ++nextWatcherBatchGeneration;
|
|
2514
2295
|
// Fast dispatch bypasses the pending-batch ingress, so preserve both
|
|
2515
2296
|
// of its contracts here: only selected routes may run, and V2 sees
|
|
2516
2297
|
// the same dirty signal as the compatibility drain.
|
|
2517
2298
|
realtimeState.scheduler?.signal("watcher");
|
|
2518
|
-
void runScopedDrain(lanes.fast, generation, true
|
|
2299
|
+
void runScopedDrain(lanes.fast, generation, true).then((exit) => {
|
|
2519
2300
|
if (exit === 0)
|
|
2520
2301
|
return;
|
|
2521
2302
|
restorePendingWatcherPaths(lanes.fast.paths, generation);
|
|
@@ -2564,36 +2345,29 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2564
2345
|
watcherRediscoveryRetries = 0;
|
|
2565
2346
|
process.stderr.write(`hq-sync-runner: event-push watcher active (watched-paths=${watchedPaths ?? "unknown"})\n`);
|
|
2566
2347
|
const startedAt = Date.now();
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
pathsFound: catchup.batch.paths.size,
|
|
2583
|
-
scannedPaths: catchup.scannedPaths,
|
|
2584
|
-
elapsedMs: Date.now() - startedAt,
|
|
2585
|
-
queued,
|
|
2586
|
-
}) + "\n");
|
|
2587
|
-
if (queued)
|
|
2588
|
-
interruptWait();
|
|
2589
|
-
}
|
|
2590
|
-
catch (err) {
|
|
2591
|
-
process.stderr.write(`hq-sync-runner: watcher warm-up catch-up failed: ${describeError(err)}\n`);
|
|
2592
|
-
// A scan failure must not silently extend the warm-up blind spot.
|
|
2593
|
-
pendingWatcherWarmupCatchupFailed = true;
|
|
2348
|
+
try {
|
|
2349
|
+
const catchup = watcher.collectWarmupCatchup?.(serviceStartedAt - WATCHER_WARMUP_SAFETY_MARGIN_MS, warmupJournalPaths());
|
|
2350
|
+
if (!catchup)
|
|
2351
|
+
return;
|
|
2352
|
+
const queued = addPendingWatcherChange(undefined, catchup.batch);
|
|
2353
|
+
pendingWatcherWarmupCatchup ||= queued;
|
|
2354
|
+
process.stderr.write(JSON.stringify({
|
|
2355
|
+
type: "watcher-warmup-catchup",
|
|
2356
|
+
reason: "watch-set-ready",
|
|
2357
|
+
pathsFound: catchup.batch.paths.size,
|
|
2358
|
+
scannedPaths: catchup.scannedPaths,
|
|
2359
|
+
elapsedMs: Date.now() - startedAt,
|
|
2360
|
+
queued,
|
|
2361
|
+
}) + "\n");
|
|
2362
|
+
if (queued)
|
|
2594
2363
|
interruptWait();
|
|
2595
|
-
|
|
2596
|
-
|
|
2364
|
+
}
|
|
2365
|
+
catch (err) {
|
|
2366
|
+
process.stderr.write(`hq-sync-runner: watcher warm-up catch-up failed: ${describeError(err)}\n`);
|
|
2367
|
+
// A scan failure must not silently extend the warm-up blind spot.
|
|
2368
|
+
pendingWatcherWarmupCatchupFailed = true;
|
|
2369
|
+
interruptWait();
|
|
2370
|
+
}
|
|
2597
2371
|
};
|
|
2598
2372
|
if (watcher.onReady)
|
|
2599
2373
|
watcher.onReady(reportWatcherReady);
|
|
@@ -2641,7 +2415,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2641
2415
|
// Submit every route in this lane before awaiting any result. In
|
|
2642
2416
|
// particular, a later route's fast receipt must never wait for an
|
|
2643
2417
|
// earlier route's slow pull to finish.
|
|
2644
|
-
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
|
|
2418
|
+
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))));
|
|
2645
2419
|
for (const execution of completed) {
|
|
2646
2420
|
if (!execution.accepted || passExitCode(execution.result) !== 0) {
|
|
2647
2421
|
throw new Error(`targeted pull failed with exit code ${passExitCode(execution.result)}`);
|
|
@@ -2695,7 +2469,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2695
2469
|
}
|
|
2696
2470
|
for (const { route, paths } of pathsByRoute.values()) {
|
|
2697
2471
|
const targetedArgv = buildTargetedPullArgv(route, passArgv, paths);
|
|
2698
|
-
const result = await runGuarded(targetedArgv, undefined, lane
|
|
2472
|
+
const result = await runGuarded(targetedArgv, undefined, lane);
|
|
2699
2473
|
if (passExitCode(result) !== 0) {
|
|
2700
2474
|
throw new Error(`targeted pull failed with exit code ${passExitCode(result)}`);
|
|
2701
2475
|
}
|
|
@@ -2743,14 +2517,14 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2743
2517
|
const execution = await runRealtimePass("wake", "pull", [], "slow", async () => runGuardedTask(() => runTaskWithLock(async () => {
|
|
2744
2518
|
await drain();
|
|
2745
2519
|
return 0;
|
|
2746
|
-
}
|
|
2520
|
+
})));
|
|
2747
2521
|
outcome = execution.accepted ? execution.result : 1;
|
|
2748
2522
|
}
|
|
2749
2523
|
else {
|
|
2750
2524
|
outcome = await runGuardedTask(() => runTaskWithLock(async () => {
|
|
2751
2525
|
await drain();
|
|
2752
2526
|
return 0;
|
|
2753
|
-
}
|
|
2527
|
+
}));
|
|
2754
2528
|
}
|
|
2755
2529
|
}
|
|
2756
2530
|
catch (err) {
|
|
@@ -2946,8 +2720,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
2946
2720
|
targetedJournalSessions: () => targetedJournalSessionCensusSizes().targetedJournalSessions,
|
|
2947
2721
|
targetedJournalRows: () => targetedJournalSessionCensusSizes().targetedJournalRows,
|
|
2948
2722
|
cachedAreaJournalMigrations: () => journalCacheCensusSizes().cachedAreaJournalMigrations,
|
|
2949
|
-
pendingSlowPasses: () =>
|
|
2950
|
-
pendingFastPasses: () =>
|
|
2723
|
+
pendingSlowPasses: () => pendingSlowPasses.length,
|
|
2724
|
+
pendingFastPasses: () => pendingFastPasses.length,
|
|
2951
2725
|
},
|
|
2952
2726
|
eventSyncSizes: () => eventSync?.censusSizes?.(),
|
|
2953
2727
|
watcherSizes: () => watcher?.censusSizes?.(),
|
|
@@ -3297,14 +3071,11 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
3297
3071
|
quarantinedDeleteRoots.delete(root);
|
|
3298
3072
|
}
|
|
3299
3073
|
// Delete snapshot capture needs a fresh keyed view while an episode is
|
|
3300
|
-
// being assembled, but completed watch cycles do not.
|
|
3301
|
-
//
|
|
3302
|
-
//
|
|
3303
|
-
// makes the next scoped callback synchronously re-pack the whole vault.
|
|
3304
|
-
// Keep it for this runner's lifetime; layout/phase changes still close
|
|
3305
|
-
// it in openAreaMigrationIfAuthoritative(), and final shutdown releases
|
|
3306
|
-
// every bridge below.
|
|
3074
|
+
// being assembled, but completed watch cycles do not. Releasing it here
|
|
3075
|
+
// prevents the watcher from retaining another full-vault projection next
|
|
3076
|
+
// to the targeted-pull and area-ledger views.
|
|
3307
3077
|
releaseWatcherJournalStores();
|
|
3078
|
+
releaseCachedAreaJournalMigrations();
|
|
3308
3079
|
const exitCode = passExitCode(result);
|
|
3309
3080
|
if (exitCode === runtime.authRequiredPassExit) {
|
|
3310
3081
|
// The pass already emitted auth-error. Hand the sentinel to the
|
|
@@ -3339,19 +3110,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
3339
3110
|
: "") +
|
|
3340
3111
|
"; staying alive, retrying next poll\n");
|
|
3341
3112
|
}
|
|
3342
|
-
else if (exitCode === UNRESOLVED_COMPANY_EXIT) {
|
|
3343
|
-
// HQ-SYNC-COMPANY-ROUTE-UID: routed paths are derived from local
|
|
3344
|
-
// company directory names, which may not be memberships. The runner
|
|
3345
|
-
// has already emitted the resolution error; terminating here would
|
|
3346
|
-
// restart the outpost watcher every 60 seconds for each such route.
|
|
3347
|
-
// Keep the watcher alive while preserving the one-shot non-zero code.
|
|
3348
|
-
process.stderr.write("hq-sync-runner: watch pass skipped — unresolved company target; retrying next poll\n");
|
|
3349
|
-
}
|
|
3350
3113
|
else if (exitCode !== 0) {
|
|
3351
|
-
|
|
3352
|
-
// pass. The error was emitted by the runner; terminating the daemon
|
|
3353
|
-
// recreates the startup blind window and strands newer watcher work.
|
|
3354
|
-
process.stderr.write(`hq-sync-runner: watch pass failed (exit=${exitCode}); staying alive, retrying next poll\n`);
|
|
3114
|
+
return exitCode;
|
|
3355
3115
|
}
|
|
3356
3116
|
const completedPostRepairPass = !enqueuedFullChore && (typeof result === "number"
|
|
3357
3117
|
? exitCode === 0
|
|
@@ -3455,10 +3215,6 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
|
|
|
3455
3215
|
releaseWatcherJournalStores();
|
|
3456
3216
|
releaseCachedAreaJournalMigrations();
|
|
3457
3217
|
await shutdown();
|
|
3458
|
-
// Coordinator shutdown waits for any active pass before the retained
|
|
3459
|
-
// projections are dropped. A shutdown must never evict a store that a
|
|
3460
|
-
// final scoped writer is still refreshing or committing through.
|
|
3461
|
-
disposeScopedJournalStores();
|
|
3462
3218
|
detachSignal?.();
|
|
3463
3219
|
}
|
|
3464
3220
|
}
|