hotsheet 0.18.1-beta.3 → 0.19.0-beta.2
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/cli.js +771 -188
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -15198,6 +15198,132 @@ var init_zod = __esm({
|
|
|
15198
15198
|
}
|
|
15199
15199
|
});
|
|
15200
15200
|
|
|
15201
|
+
// src/hashWorker.ts
|
|
15202
|
+
var hashWorker_exports = {};
|
|
15203
|
+
__export(hashWorker_exports, {
|
|
15204
|
+
_resetHashWorkerForTests: () => _resetHashWorkerForTests,
|
|
15205
|
+
hashFileInProcess: () => hashFileInProcess,
|
|
15206
|
+
hashFileOffThread: () => hashFileOffThread,
|
|
15207
|
+
terminateHashWorker: () => terminateHashWorker
|
|
15208
|
+
});
|
|
15209
|
+
import { createHash } from "crypto";
|
|
15210
|
+
import { createReadStream } from "fs";
|
|
15211
|
+
import { pipeline } from "stream/promises";
|
|
15212
|
+
import { Worker } from "worker_threads";
|
|
15213
|
+
function failWorker(err) {
|
|
15214
|
+
for (const p of pending.values()) p.reject(err);
|
|
15215
|
+
pending.clear();
|
|
15216
|
+
if (worker !== null) {
|
|
15217
|
+
const w = worker;
|
|
15218
|
+
worker = null;
|
|
15219
|
+
void w.terminate().catch(() => {
|
|
15220
|
+
});
|
|
15221
|
+
}
|
|
15222
|
+
crashCount++;
|
|
15223
|
+
if (crashCount >= MAX_WORKER_CRASHES) workerUnusable = true;
|
|
15224
|
+
}
|
|
15225
|
+
function ensureWorker() {
|
|
15226
|
+
if (workerUnusable) return null;
|
|
15227
|
+
if (worker !== null) return worker;
|
|
15228
|
+
try {
|
|
15229
|
+
const w = new Worker(WORKER_SOURCE, { eval: true });
|
|
15230
|
+
w.on("message", (msg) => {
|
|
15231
|
+
const p = pending.get(msg.id);
|
|
15232
|
+
if (p === void 0) return;
|
|
15233
|
+
pending.delete(msg.id);
|
|
15234
|
+
if (msg.error !== void 0) p.reject(new Error(msg.error));
|
|
15235
|
+
else p.resolve({ sha: msg.sha ?? "", size: msg.size ?? 0 });
|
|
15236
|
+
});
|
|
15237
|
+
w.on("error", (err) => {
|
|
15238
|
+
failWorker(err);
|
|
15239
|
+
});
|
|
15240
|
+
w.on("exit", (code) => {
|
|
15241
|
+
if (code !== 0) failWorker(new Error(`hash worker exited with code ${code.toString()}`));
|
|
15242
|
+
});
|
|
15243
|
+
w.unref();
|
|
15244
|
+
worker = w;
|
|
15245
|
+
return w;
|
|
15246
|
+
} catch {
|
|
15247
|
+
workerUnusable = true;
|
|
15248
|
+
return null;
|
|
15249
|
+
}
|
|
15250
|
+
}
|
|
15251
|
+
async function hashFileOffThread(path) {
|
|
15252
|
+
const w = ensureWorker();
|
|
15253
|
+
if (w === null) return hashFileInProcess(path);
|
|
15254
|
+
return new Promise((resolve11, reject2) => {
|
|
15255
|
+
const id = nextId++;
|
|
15256
|
+
pending.set(id, { resolve: resolve11, reject: reject2 });
|
|
15257
|
+
try {
|
|
15258
|
+
w.postMessage({ id, path });
|
|
15259
|
+
} catch (err) {
|
|
15260
|
+
pending.delete(id);
|
|
15261
|
+
reject2(err instanceof Error ? err : new Error(String(err)));
|
|
15262
|
+
}
|
|
15263
|
+
});
|
|
15264
|
+
}
|
|
15265
|
+
async function hashFileInProcess(path) {
|
|
15266
|
+
const hash2 = createHash("sha256");
|
|
15267
|
+
let size = 0;
|
|
15268
|
+
await pipeline(
|
|
15269
|
+
createReadStream(path),
|
|
15270
|
+
async function* (source) {
|
|
15271
|
+
for await (const chunk of source) {
|
|
15272
|
+
size += chunk.length;
|
|
15273
|
+
hash2.update(chunk);
|
|
15274
|
+
yield chunk;
|
|
15275
|
+
}
|
|
15276
|
+
},
|
|
15277
|
+
async function(source) {
|
|
15278
|
+
for await (const _ of source) {
|
|
15279
|
+
}
|
|
15280
|
+
}
|
|
15281
|
+
);
|
|
15282
|
+
return { sha: hash2.digest("hex"), size };
|
|
15283
|
+
}
|
|
15284
|
+
async function terminateHashWorker() {
|
|
15285
|
+
const w = worker;
|
|
15286
|
+
worker = null;
|
|
15287
|
+
pending.clear();
|
|
15288
|
+
if (w !== null) {
|
|
15289
|
+
try {
|
|
15290
|
+
await w.terminate();
|
|
15291
|
+
} catch {
|
|
15292
|
+
}
|
|
15293
|
+
}
|
|
15294
|
+
}
|
|
15295
|
+
function _resetHashWorkerForTests() {
|
|
15296
|
+
void terminateHashWorker();
|
|
15297
|
+
workerUnusable = false;
|
|
15298
|
+
crashCount = 0;
|
|
15299
|
+
pending.clear();
|
|
15300
|
+
}
|
|
15301
|
+
var WORKER_SOURCE, MAX_WORKER_CRASHES, worker, workerUnusable, crashCount, nextId, pending;
|
|
15302
|
+
var init_hashWorker = __esm({
|
|
15303
|
+
"src/hashWorker.ts"() {
|
|
15304
|
+
"use strict";
|
|
15305
|
+
WORKER_SOURCE = `
|
|
15306
|
+
const { parentPort } = require('worker_threads');
|
|
15307
|
+
const { createHash } = require('crypto');
|
|
15308
|
+
const { createReadStream } = require('fs');
|
|
15309
|
+
parentPort.on('message', (msg) => {
|
|
15310
|
+
const hash = createHash('sha256');
|
|
15311
|
+
let size = 0;
|
|
15312
|
+
const stream = createReadStream(msg.path);
|
|
15313
|
+
stream.on('data', (chunk) => { size += chunk.length; hash.update(chunk); });
|
|
15314
|
+
stream.on('end', () => parentPort.postMessage({ id: msg.id, sha: hash.digest('hex'), size: size }));
|
|
15315
|
+
stream.on('error', (err) => parentPort.postMessage({ id: msg.id, error: (err && err.message) ? err.message : String(err) }));
|
|
15316
|
+
});
|
|
15317
|
+
`;
|
|
15318
|
+
MAX_WORKER_CRASHES = 3;
|
|
15319
|
+
worker = null;
|
|
15320
|
+
workerUnusable = false;
|
|
15321
|
+
crashCount = 0;
|
|
15322
|
+
nextId = 1;
|
|
15323
|
+
pending = /* @__PURE__ */ new Map();
|
|
15324
|
+
}
|
|
15325
|
+
});
|
|
15326
|
+
|
|
15201
15327
|
// src/attachmentBackup.ts
|
|
15202
15328
|
var attachmentBackup_exports = {};
|
|
15203
15329
|
__export(attachmentBackup_exports, {
|
|
@@ -15214,10 +15340,8 @@ __export(attachmentBackup_exports, {
|
|
|
15214
15340
|
runAttachmentGc: () => runAttachmentGc,
|
|
15215
15341
|
writeManifestAtomically: () => writeManifestAtomically
|
|
15216
15342
|
});
|
|
15217
|
-
import { createHash } from "crypto";
|
|
15218
15343
|
import {
|
|
15219
15344
|
copyFileSync,
|
|
15220
|
-
createReadStream,
|
|
15221
15345
|
existsSync,
|
|
15222
15346
|
mkdirSync,
|
|
15223
15347
|
promises as fsp,
|
|
@@ -15227,7 +15351,6 @@ import {
|
|
|
15227
15351
|
statSync
|
|
15228
15352
|
} from "fs";
|
|
15229
15353
|
import { join } from "path";
|
|
15230
|
-
import { pipeline } from "stream/promises";
|
|
15231
15354
|
import { gunzipSync } from "zlib";
|
|
15232
15355
|
function manifestSiblingFilename(tarballFilename) {
|
|
15233
15356
|
if (!tarballFilename.endsWith(".tar.gz")) {
|
|
@@ -15236,24 +15359,7 @@ function manifestSiblingFilename(tarballFilename) {
|
|
|
15236
15359
|
return `${tarballFilename.slice(0, -".tar.gz".length)}.attachments.json`;
|
|
15237
15360
|
}
|
|
15238
15361
|
async function hashFile(path) {
|
|
15239
|
-
|
|
15240
|
-
let size = 0;
|
|
15241
|
-
await pipeline(
|
|
15242
|
-
createReadStream(path),
|
|
15243
|
-
async function* (source) {
|
|
15244
|
-
for await (const chunk of source) {
|
|
15245
|
-
size += chunk.length;
|
|
15246
|
-
hash2.update(chunk);
|
|
15247
|
-
yield chunk;
|
|
15248
|
-
}
|
|
15249
|
-
},
|
|
15250
|
-
// Sink: just consume the bytes; pipeline needs a writable end.
|
|
15251
|
-
async function(source) {
|
|
15252
|
-
for await (const _ of source) {
|
|
15253
|
-
}
|
|
15254
|
-
}
|
|
15255
|
-
);
|
|
15256
|
-
return { sha: hash2.digest("hex"), size };
|
|
15362
|
+
return hashFileOffThread(path);
|
|
15257
15363
|
}
|
|
15258
15364
|
function attachmentBlobsDir(backupRoot) {
|
|
15259
15365
|
return join(backupRoot, "attachments");
|
|
@@ -15400,6 +15506,7 @@ async function runAttachmentGc(backupRoot) {
|
|
|
15400
15506
|
const manifestPaths = collectManifestPaths(backupRoot);
|
|
15401
15507
|
const liveShas = /* @__PURE__ */ new Set();
|
|
15402
15508
|
let parseFailure = false;
|
|
15509
|
+
let scanned = 0;
|
|
15403
15510
|
for (const p of manifestPaths) {
|
|
15404
15511
|
const m = readManifest(p);
|
|
15405
15512
|
if (m === null) {
|
|
@@ -15408,23 +15515,27 @@ async function runAttachmentGc(backupRoot) {
|
|
|
15408
15515
|
break;
|
|
15409
15516
|
}
|
|
15410
15517
|
for (const e of m.entries) liveShas.add(e.sha);
|
|
15518
|
+
if (++scanned % 25 === 0) await yieldToEventLoop();
|
|
15411
15519
|
}
|
|
15412
15520
|
if (parseFailure) {
|
|
15413
15521
|
return { deleted: 0, bytesReclaimed: 0, scannedManifests: manifestPaths.length, skippedDueToParseFailure: true };
|
|
15414
15522
|
}
|
|
15415
15523
|
let deleted = 0;
|
|
15416
15524
|
let bytesReclaimed = 0;
|
|
15417
|
-
|
|
15525
|
+
const blobNames = await fsp.readdir(blobsDir);
|
|
15526
|
+
let iterated = 0;
|
|
15527
|
+
for (const name of blobNames) {
|
|
15528
|
+
if (++iterated % 500 === 0) await yieldToEventLoop();
|
|
15418
15529
|
if (name.endsWith(".tmp")) continue;
|
|
15419
15530
|
if (liveShas.has(name)) continue;
|
|
15420
15531
|
const p = join(blobsDir, name);
|
|
15421
15532
|
let size = 0;
|
|
15422
15533
|
try {
|
|
15423
|
-
size =
|
|
15534
|
+
size = (await fsp.stat(p)).size;
|
|
15424
15535
|
} catch {
|
|
15425
15536
|
}
|
|
15426
15537
|
try {
|
|
15427
|
-
|
|
15538
|
+
await fsp.rm(p, { force: true });
|
|
15428
15539
|
deleted++;
|
|
15429
15540
|
bytesReclaimed += size;
|
|
15430
15541
|
} catch (err) {
|
|
@@ -15634,6 +15745,7 @@ var init_attachmentBackup = __esm({
|
|
|
15634
15745
|
"src/attachmentBackup.ts"() {
|
|
15635
15746
|
"use strict";
|
|
15636
15747
|
init_zod();
|
|
15748
|
+
init_hashWorker();
|
|
15637
15749
|
ATTACHMENT_MANIFEST_VERSION = 1;
|
|
15638
15750
|
JsonCosaveAttachmentRowSchema = external_exports.object({
|
|
15639
15751
|
id: external_exports.number(),
|
|
@@ -15949,10 +16061,14 @@ __export(freezeLogger_exports, {
|
|
|
15949
16061
|
FREEZE_LOG_MAX_BYTES: () => FREEZE_LOG_MAX_BYTES,
|
|
15950
16062
|
FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE: () => FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE,
|
|
15951
16063
|
LONG_TASK_THRESHOLD_MS: () => LONG_TASK_THRESHOLD_MS,
|
|
16064
|
+
WAKE_GAP_THRESHOLD_MS: () => WAKE_GAP_THRESHOLD_MS,
|
|
15952
16065
|
_resetForTesting: () => _resetForTesting,
|
|
16066
|
+
_simulateHeartbeatGapForTesting: () => _simulateHeartbeatGapForTesting,
|
|
15953
16067
|
appendFreezeLog: () => appendFreezeLog,
|
|
16068
|
+
getRecentEventLoopLagMs: () => getRecentEventLoopLagMs,
|
|
15954
16069
|
instrumentAsync: () => instrumentAsync,
|
|
15955
16070
|
instrumentSync: () => instrumentSync,
|
|
16071
|
+
onServerWake: () => onServerWake,
|
|
15956
16072
|
startServerEventLoopHeartbeat: () => startServerEventLoopHeartbeat,
|
|
15957
16073
|
stopServerEventLoopHeartbeat: () => stopServerEventLoopHeartbeat
|
|
15958
16074
|
});
|
|
@@ -16011,6 +16127,44 @@ async function rotateIfNeeded(path, pendingBytes) {
|
|
|
16011
16127
|
console.warn("[hotsheet freeze.log] rotate writeFile failed:", err instanceof Error ? err.message : String(err));
|
|
16012
16128
|
}
|
|
16013
16129
|
}
|
|
16130
|
+
function onServerWake(listener) {
|
|
16131
|
+
wakeListeners.add(listener);
|
|
16132
|
+
return () => {
|
|
16133
|
+
wakeListeners.delete(listener);
|
|
16134
|
+
};
|
|
16135
|
+
}
|
|
16136
|
+
function handleHeartbeatGap(blockMs) {
|
|
16137
|
+
if (blockMs >= WAKE_GAP_THRESHOLD_MS) {
|
|
16138
|
+
lastEventLoopLagMs = 0;
|
|
16139
|
+
if (heartbeatDataDir !== null) {
|
|
16140
|
+
void appendFreezeLog(heartbeatDataDir, {
|
|
16141
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16142
|
+
source: "server-wake",
|
|
16143
|
+
durationMs: Math.round(blockMs),
|
|
16144
|
+
context: `resumed from suspend after ~${Math.round(blockMs / 1e3).toString()}s`
|
|
16145
|
+
});
|
|
16146
|
+
}
|
|
16147
|
+
for (const listener of wakeListeners) {
|
|
16148
|
+
try {
|
|
16149
|
+
listener(blockMs);
|
|
16150
|
+
} catch {
|
|
16151
|
+
}
|
|
16152
|
+
}
|
|
16153
|
+
return;
|
|
16154
|
+
}
|
|
16155
|
+
lastEventLoopLagMs = blockMs > 0 ? blockMs : 0;
|
|
16156
|
+
if (blockMs >= LONG_TASK_THRESHOLD_MS && heartbeatDataDir !== null) {
|
|
16157
|
+
void appendFreezeLog(heartbeatDataDir, {
|
|
16158
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16159
|
+
source: "server-heartbeat",
|
|
16160
|
+
durationMs: Math.round(blockMs),
|
|
16161
|
+
context: "event-loop blocked"
|
|
16162
|
+
});
|
|
16163
|
+
}
|
|
16164
|
+
}
|
|
16165
|
+
function _simulateHeartbeatGapForTesting(blockMs) {
|
|
16166
|
+
handleHeartbeatGap(blockMs);
|
|
16167
|
+
}
|
|
16014
16168
|
function startServerEventLoopHeartbeat(dataDir) {
|
|
16015
16169
|
if (heartbeatTimer !== null) return;
|
|
16016
16170
|
heartbeatDataDir = dataDir;
|
|
@@ -16019,18 +16173,13 @@ function startServerEventLoopHeartbeat(dataDir) {
|
|
|
16019
16173
|
const now = process.hrtime.bigint();
|
|
16020
16174
|
const elapsedMs = Number(now - lastHeartbeatNs) / 1e6;
|
|
16021
16175
|
lastHeartbeatNs = now;
|
|
16022
|
-
|
|
16023
|
-
if (blockMs >= LONG_TASK_THRESHOLD_MS && heartbeatDataDir !== null) {
|
|
16024
|
-
void appendFreezeLog(heartbeatDataDir, {
|
|
16025
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16026
|
-
source: "server-heartbeat",
|
|
16027
|
-
durationMs: Math.round(blockMs),
|
|
16028
|
-
context: "event-loop blocked"
|
|
16029
|
-
});
|
|
16030
|
-
}
|
|
16176
|
+
handleHeartbeatGap(elapsedMs - HEARTBEAT_INTERVAL_MS);
|
|
16031
16177
|
}, HEARTBEAT_INTERVAL_MS);
|
|
16032
16178
|
heartbeatTimer.unref();
|
|
16033
16179
|
}
|
|
16180
|
+
function getRecentEventLoopLagMs() {
|
|
16181
|
+
return lastEventLoopLagMs;
|
|
16182
|
+
}
|
|
16034
16183
|
function stopServerEventLoopHeartbeat() {
|
|
16035
16184
|
if (heartbeatTimer !== null) {
|
|
16036
16185
|
clearInterval(heartbeatTimer);
|
|
@@ -16077,9 +16226,11 @@ function _resetForTesting() {
|
|
|
16077
16226
|
}
|
|
16078
16227
|
heartbeatDataDir = null;
|
|
16079
16228
|
lastHeartbeatNs = 0n;
|
|
16229
|
+
lastEventLoopLagMs = 0;
|
|
16230
|
+
wakeListeners.clear();
|
|
16080
16231
|
appendQueue.clear();
|
|
16081
16232
|
}
|
|
16082
|
-
var FREEZE_LOG_FILENAME, LONG_TASK_THRESHOLD_MS, FREEZE_LOG_MAX_BYTES, FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE, HEARTBEAT_INTERVAL_MS, appendQueue, heartbeatTimer, lastHeartbeatNs, heartbeatDataDir;
|
|
16233
|
+
var FREEZE_LOG_FILENAME, LONG_TASK_THRESHOLD_MS, FREEZE_LOG_MAX_BYTES, FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE, HEARTBEAT_INTERVAL_MS, WAKE_GAP_THRESHOLD_MS, appendQueue, heartbeatTimer, lastHeartbeatNs, lastEventLoopLagMs, heartbeatDataDir, wakeListeners;
|
|
16083
16234
|
var init_freezeLogger = __esm({
|
|
16084
16235
|
"src/diagnostics/freezeLogger.ts"() {
|
|
16085
16236
|
"use strict";
|
|
@@ -16088,10 +16239,213 @@ var init_freezeLogger = __esm({
|
|
|
16088
16239
|
FREEZE_LOG_MAX_BYTES = 1048576;
|
|
16089
16240
|
FREEZE_LOG_TARGET_BYTES_AFTER_TRUNCATE = 524288;
|
|
16090
16241
|
HEARTBEAT_INTERVAL_MS = 50;
|
|
16242
|
+
WAKE_GAP_THRESHOLD_MS = 1e4;
|
|
16091
16243
|
appendQueue = /* @__PURE__ */ new Map();
|
|
16092
16244
|
heartbeatTimer = null;
|
|
16093
16245
|
lastHeartbeatNs = 0n;
|
|
16246
|
+
lastEventLoopLagMs = 0;
|
|
16094
16247
|
heartbeatDataDir = null;
|
|
16248
|
+
wakeListeners = /* @__PURE__ */ new Set();
|
|
16249
|
+
}
|
|
16250
|
+
});
|
|
16251
|
+
|
|
16252
|
+
// src/scheduler/backgroundScheduler.ts
|
|
16253
|
+
var backgroundScheduler_exports = {};
|
|
16254
|
+
__export(backgroundScheduler_exports, {
|
|
16255
|
+
PRIORITY: () => PRIORITY,
|
|
16256
|
+
_resetDefaultSchedulerForTests: () => _resetDefaultSchedulerForTests,
|
|
16257
|
+
createBackgroundScheduler: () => createBackgroundScheduler,
|
|
16258
|
+
getBackgroundScheduler: () => getBackgroundScheduler
|
|
16259
|
+
});
|
|
16260
|
+
function createBackgroundScheduler(opts = {}) {
|
|
16261
|
+
const concurrency = opts.concurrency ?? 2;
|
|
16262
|
+
const lagProvider = opts.lagProvider ?? getRecentEventLoopLagMs;
|
|
16263
|
+
const lagThresholdMs = opts.lagThresholdMs ?? 200;
|
|
16264
|
+
const reDrainDelayMs = opts.reDrainDelayMs ?? 250;
|
|
16265
|
+
const now = opts.now ?? (() => Date.now());
|
|
16266
|
+
const wakeStaggerWindowMs = opts.wakeStaggerWindowMs ?? 15e3;
|
|
16267
|
+
const wakeStaggerStepMs = opts.wakeStaggerStepMs ?? 250;
|
|
16268
|
+
const onError = opts.onError ?? ((key, err) => {
|
|
16269
|
+
console.warn("[hotsheet backgroundScheduler] job failed:", key, err instanceof Error ? err.message : String(err));
|
|
16270
|
+
});
|
|
16271
|
+
const pending2 = /* @__PURE__ */ new Map();
|
|
16272
|
+
const running = /* @__PURE__ */ new Set();
|
|
16273
|
+
const runningGroups = /* @__PURE__ */ new Set();
|
|
16274
|
+
const awaiters = /* @__PURE__ */ new Map();
|
|
16275
|
+
const runningAwaiters = /* @__PURE__ */ new Map();
|
|
16276
|
+
let seq = 0;
|
|
16277
|
+
const lastServedSeq = /* @__PURE__ */ new Map();
|
|
16278
|
+
let reDrainTimer = null;
|
|
16279
|
+
const idleWaiters = [];
|
|
16280
|
+
let staggerUntil = 0;
|
|
16281
|
+
let lastStartAt = Number.NEGATIVE_INFINITY;
|
|
16282
|
+
let staggerTimer = null;
|
|
16283
|
+
function projectKeyOf(job) {
|
|
16284
|
+
return job.projectKey ?? job.key;
|
|
16285
|
+
}
|
|
16286
|
+
function settleIdleIfDone() {
|
|
16287
|
+
if (running.size === 0 && pending2.size === 0 && idleWaiters.length > 0) {
|
|
16288
|
+
const waiters = idleWaiters.splice(0, idleWaiters.length);
|
|
16289
|
+
for (const w of waiters) w();
|
|
16290
|
+
}
|
|
16291
|
+
}
|
|
16292
|
+
function pickNext(highLag) {
|
|
16293
|
+
let best = null;
|
|
16294
|
+
let lagDeferred = false;
|
|
16295
|
+
for (const job of pending2.values()) {
|
|
16296
|
+
if (running.has(job.key)) continue;
|
|
16297
|
+
if (job.exclusiveGroup !== void 0 && runningGroups.has(job.exclusiveGroup)) continue;
|
|
16298
|
+
if (highLag && job.deferUnderLag === true) {
|
|
16299
|
+
lagDeferred = true;
|
|
16300
|
+
continue;
|
|
16301
|
+
}
|
|
16302
|
+
if (best === null) {
|
|
16303
|
+
best = job;
|
|
16304
|
+
continue;
|
|
16305
|
+
}
|
|
16306
|
+
if (job.priority !== best.priority) {
|
|
16307
|
+
if (job.priority < best.priority) best = job;
|
|
16308
|
+
continue;
|
|
16309
|
+
}
|
|
16310
|
+
const a = lastServedSeq.get(projectKeyOf(job)) ?? -1;
|
|
16311
|
+
const b = lastServedSeq.get(projectKeyOf(best)) ?? -1;
|
|
16312
|
+
if (a < b) best = job;
|
|
16313
|
+
}
|
|
16314
|
+
return { job: best, lagDeferred };
|
|
16315
|
+
}
|
|
16316
|
+
function drain() {
|
|
16317
|
+
const highLag = lagProvider() > lagThresholdMs;
|
|
16318
|
+
const t = now();
|
|
16319
|
+
const inStagger = t < staggerUntil;
|
|
16320
|
+
const cap = inStagger ? 1 : concurrency;
|
|
16321
|
+
let armReDrain = false;
|
|
16322
|
+
while (running.size < cap) {
|
|
16323
|
+
if (inStagger) {
|
|
16324
|
+
const sinceLast = t - lastStartAt;
|
|
16325
|
+
if (sinceLast < wakeStaggerStepMs) {
|
|
16326
|
+
scheduleStaggerDrain(wakeStaggerStepMs - sinceLast);
|
|
16327
|
+
break;
|
|
16328
|
+
}
|
|
16329
|
+
}
|
|
16330
|
+
const { job, lagDeferred } = pickNext(highLag);
|
|
16331
|
+
if (job === null) {
|
|
16332
|
+
armReDrain = lagDeferred;
|
|
16333
|
+
break;
|
|
16334
|
+
}
|
|
16335
|
+
pending2.delete(job.key);
|
|
16336
|
+
running.add(job.key);
|
|
16337
|
+
if (job.exclusiveGroup !== void 0) runningGroups.add(job.exclusiveGroup);
|
|
16338
|
+
runningAwaiters.set(job.key, awaiters.get(job.key) ?? []);
|
|
16339
|
+
awaiters.delete(job.key);
|
|
16340
|
+
lastServedSeq.set(projectKeyOf(job), ++seq);
|
|
16341
|
+
lastStartAt = t;
|
|
16342
|
+
void runJob(job);
|
|
16343
|
+
}
|
|
16344
|
+
if (armReDrain) scheduleReDrain();
|
|
16345
|
+
settleIdleIfDone();
|
|
16346
|
+
}
|
|
16347
|
+
async function runJob(job) {
|
|
16348
|
+
try {
|
|
16349
|
+
await job.run();
|
|
16350
|
+
} catch (err) {
|
|
16351
|
+
onError(job.key, err);
|
|
16352
|
+
} finally {
|
|
16353
|
+
running.delete(job.key);
|
|
16354
|
+
if (job.exclusiveGroup !== void 0) runningGroups.delete(job.exclusiveGroup);
|
|
16355
|
+
const settled = runningAwaiters.get(job.key);
|
|
16356
|
+
runningAwaiters.delete(job.key);
|
|
16357
|
+
if (settled !== void 0) for (const r of settled) r();
|
|
16358
|
+
drain();
|
|
16359
|
+
}
|
|
16360
|
+
}
|
|
16361
|
+
function scheduleReDrain() {
|
|
16362
|
+
if (reDrainTimer !== null) return;
|
|
16363
|
+
reDrainTimer = setTimeout(() => {
|
|
16364
|
+
reDrainTimer = null;
|
|
16365
|
+
drain();
|
|
16366
|
+
}, reDrainDelayMs);
|
|
16367
|
+
reDrainTimer.unref();
|
|
16368
|
+
}
|
|
16369
|
+
function scheduleStaggerDrain(delayMs) {
|
|
16370
|
+
if (staggerTimer !== null) clearTimeout(staggerTimer);
|
|
16371
|
+
staggerTimer = setTimeout(() => {
|
|
16372
|
+
staggerTimer = null;
|
|
16373
|
+
drain();
|
|
16374
|
+
}, Math.max(0, delayMs));
|
|
16375
|
+
staggerTimer.unref();
|
|
16376
|
+
}
|
|
16377
|
+
return {
|
|
16378
|
+
submit(job) {
|
|
16379
|
+
pending2.set(job.key, job);
|
|
16380
|
+
const done = new Promise((resolve11) => {
|
|
16381
|
+
const list = awaiters.get(job.key) ?? [];
|
|
16382
|
+
list.push(resolve11);
|
|
16383
|
+
awaiters.set(job.key, list);
|
|
16384
|
+
});
|
|
16385
|
+
drain();
|
|
16386
|
+
return done;
|
|
16387
|
+
},
|
|
16388
|
+
runningCount: () => running.size,
|
|
16389
|
+
pendingCount: () => pending2.size,
|
|
16390
|
+
onIdle() {
|
|
16391
|
+
if (running.size === 0 && pending2.size === 0) return Promise.resolve();
|
|
16392
|
+
return new Promise((resolve11) => {
|
|
16393
|
+
idleWaiters.push(resolve11);
|
|
16394
|
+
});
|
|
16395
|
+
},
|
|
16396
|
+
noteWake() {
|
|
16397
|
+
staggerUntil = now() + wakeStaggerWindowMs;
|
|
16398
|
+
drain();
|
|
16399
|
+
},
|
|
16400
|
+
clear() {
|
|
16401
|
+
pending2.clear();
|
|
16402
|
+
if (reDrainTimer !== null) {
|
|
16403
|
+
clearTimeout(reDrainTimer);
|
|
16404
|
+
reDrainTimer = null;
|
|
16405
|
+
}
|
|
16406
|
+
if (staggerTimer !== null) {
|
|
16407
|
+
clearTimeout(staggerTimer);
|
|
16408
|
+
staggerTimer = null;
|
|
16409
|
+
}
|
|
16410
|
+
staggerUntil = 0;
|
|
16411
|
+
const orphaned = [...awaiters.values()].flat();
|
|
16412
|
+
awaiters.clear();
|
|
16413
|
+
for (const r of orphaned) r();
|
|
16414
|
+
settleIdleIfDone();
|
|
16415
|
+
}
|
|
16416
|
+
};
|
|
16417
|
+
}
|
|
16418
|
+
function getBackgroundScheduler() {
|
|
16419
|
+
if (defaultScheduler === null) {
|
|
16420
|
+
defaultScheduler = createBackgroundScheduler();
|
|
16421
|
+
}
|
|
16422
|
+
return defaultScheduler;
|
|
16423
|
+
}
|
|
16424
|
+
function _resetDefaultSchedulerForTests() {
|
|
16425
|
+
defaultScheduler?.clear();
|
|
16426
|
+
defaultScheduler = null;
|
|
16427
|
+
}
|
|
16428
|
+
var PRIORITY, defaultScheduler;
|
|
16429
|
+
var init_backgroundScheduler = __esm({
|
|
16430
|
+
"src/scheduler/backgroundScheduler.ts"() {
|
|
16431
|
+
"use strict";
|
|
16432
|
+
init_freezeLogger();
|
|
16433
|
+
PRIORITY = {
|
|
16434
|
+
/** Startup restoration of the previous session's projects (load-resilience
|
|
16435
|
+
* epic HS-8722, docs/75 — the startup restore path). Highest
|
|
16436
|
+
* priority because each restored project is a user-visible tab — they should
|
|
16437
|
+
* fill in ahead of routine git/markdown/backup churn — but still bounded by
|
|
16438
|
+
* the scheduler's concurrency cap + lag backpressure so the serial fan-out
|
|
16439
|
+
* of N projects can't saturate the event loop on launch (the HS-8721 freeze,
|
|
16440
|
+
* on the one path never migrated onto the scheduler). */
|
|
16441
|
+
PROJECT_RESTORE: 5,
|
|
16442
|
+
GIT_STATUS: 10,
|
|
16443
|
+
MARKDOWN_SYNC: 20,
|
|
16444
|
+
SNAPSHOT: 30,
|
|
16445
|
+
BACKUP: 40,
|
|
16446
|
+
GC: 50
|
|
16447
|
+
};
|
|
16448
|
+
defaultScheduler = null;
|
|
16095
16449
|
}
|
|
16096
16450
|
});
|
|
16097
16451
|
|
|
@@ -16148,7 +16502,7 @@ function initSnapshotScheduler(dataDir) {
|
|
|
16148
16502
|
const state = getOrCreateState(dataDir);
|
|
16149
16503
|
if (state.safetyTimer !== null) return;
|
|
16150
16504
|
state.safetyTimer = setInterval(() => {
|
|
16151
|
-
if (state.dirty && !state.inProgress) void
|
|
16505
|
+
if (state.dirty && !state.inProgress) void submitSnapshotJob(dataDir);
|
|
16152
16506
|
}, numericSetting(dataDir, "db_snapshot_safety_interval_ms", DEFAULT_SAFETY_INTERVAL_MS));
|
|
16153
16507
|
state.safetyTimer.unref();
|
|
16154
16508
|
}
|
|
@@ -16161,9 +16515,20 @@ function scheduleSnapshot(dataDir) {
|
|
|
16161
16515
|
if (state.debounceTimer) clearTimeout(state.debounceTimer);
|
|
16162
16516
|
state.debounceTimer = setTimeout(() => {
|
|
16163
16517
|
state.debounceTimer = null;
|
|
16164
|
-
void
|
|
16518
|
+
void submitSnapshotJob(dir);
|
|
16165
16519
|
}, numericSetting(dir, "db_snapshot_debounce_ms", DEFAULT_DEBOUNCE_MS));
|
|
16166
16520
|
}
|
|
16521
|
+
function submitSnapshotJob(dataDir) {
|
|
16522
|
+
return getBackgroundScheduler().submit({
|
|
16523
|
+
key: `snapshot:${dataDir}`,
|
|
16524
|
+
priority: PRIORITY.SNAPSHOT,
|
|
16525
|
+
projectKey: dataDir,
|
|
16526
|
+
deferUnderLag: false,
|
|
16527
|
+
run: async () => {
|
|
16528
|
+
await writeSnapshotNow(dataDir);
|
|
16529
|
+
}
|
|
16530
|
+
});
|
|
16531
|
+
}
|
|
16167
16532
|
async function writeSnapshotNow(dataDir) {
|
|
16168
16533
|
if (!isSnapshotProtectionEnabled(dataDir)) return null;
|
|
16169
16534
|
if (!existsSync4(join5(dataDir, "db"))) return null;
|
|
@@ -16205,7 +16570,7 @@ async function snapshotAllForShutdown() {
|
|
|
16205
16570
|
for (const dir of dirs) {
|
|
16206
16571
|
if (!isSnapshotProtectionEnabled(dir)) continue;
|
|
16207
16572
|
try {
|
|
16208
|
-
await
|
|
16573
|
+
await submitSnapshotJob(dir);
|
|
16209
16574
|
} catch (err) {
|
|
16210
16575
|
console.error(`[snapshot] shutdown snapshot failed for ${dir}:`, err);
|
|
16211
16576
|
}
|
|
@@ -16257,6 +16622,7 @@ var init_snapshot = __esm({
|
|
|
16257
16622
|
"use strict";
|
|
16258
16623
|
init_freezeLogger();
|
|
16259
16624
|
init_file_settings();
|
|
16625
|
+
init_backgroundScheduler();
|
|
16260
16626
|
init_connection();
|
|
16261
16627
|
DEFAULT_DEBOUNCE_MS = 2e3;
|
|
16262
16628
|
DEFAULT_SAFETY_INTERVAL_MS = 12e4;
|
|
@@ -16570,10 +16936,10 @@ async function renameDirWithRetry(from, to) {
|
|
|
16570
16936
|
}
|
|
16571
16937
|
async function completeDeferredRecovery(dbPath) {
|
|
16572
16938
|
const dataDir = dbPath.replace(/[\\/]db$/, "");
|
|
16573
|
-
const
|
|
16574
|
-
if (
|
|
16575
|
-
if (
|
|
16576
|
-
console.error(`[db] deferred recovery gave up after ${String(
|
|
16939
|
+
const pending2 = readPendingRecovery(dataDir);
|
|
16940
|
+
if (pending2 === null) return null;
|
|
16941
|
+
if (pending2.attempts > MAX_DEFERRED_RECOVERY_ATTEMPTS) {
|
|
16942
|
+
console.error(`[db] deferred recovery gave up after ${String(pending2.attempts)} attempts; leaving the corrupt cluster for manual rescue.`);
|
|
16577
16943
|
clearPendingRecovery(dataDir);
|
|
16578
16944
|
return null;
|
|
16579
16945
|
}
|
|
@@ -16586,7 +16952,7 @@ async function completeDeferredRecovery(dbPath) {
|
|
|
16586
16952
|
try {
|
|
16587
16953
|
await renameDirWithRetry(dbPath, corruptPath);
|
|
16588
16954
|
} catch (renameErr) {
|
|
16589
|
-
writePendingRecovery(dataDir,
|
|
16955
|
+
writePendingRecovery(dataDir, pending2.attempts + 1);
|
|
16590
16956
|
console.error(`[db] deferred recovery could not move db/ yet: ${getErrorMessage(renameErr)}`);
|
|
16591
16957
|
return null;
|
|
16592
16958
|
}
|
|
@@ -16883,6 +17249,22 @@ async function initSchema(db) {
|
|
|
16883
17249
|
CREATE INDEX IF NOT EXISTS idx_otel_spans_session_ts ON otel_spans(session_id, start_ts);
|
|
16884
17250
|
CREATE INDEX IF NOT EXISTS idx_otel_spans_prompt ON otel_spans(prompt_id);
|
|
16885
17251
|
CREATE INDEX IF NOT EXISTS idx_otel_spans_trace ON otel_spans(trace_id);
|
|
17252
|
+
|
|
17253
|
+
-- HS-8730 (per-ticket cost, time-window correlation) \u2014 records when each
|
|
17254
|
+
-- ticket was actively being worked (its status was 'started'), so the
|
|
17255
|
+
-- per-ticket rollup can attribute api_request cost by timestamp instead of
|
|
17256
|
+
-- only the channelUI prompt marker. Lives in the telemetry DB (this is the
|
|
17257
|
+
-- default/primary project's DB per getTelemetryDb) so the rollup join with
|
|
17258
|
+
-- otel_events is single-DB. Keyed by project_secret (matching otel_events).
|
|
17259
|
+
CREATE TABLE IF NOT EXISTS ticket_work_intervals (
|
|
17260
|
+
id SERIAL PRIMARY KEY,
|
|
17261
|
+
project_secret TEXT NOT NULL,
|
|
17262
|
+
ticket_number TEXT NOT NULL,
|
|
17263
|
+
started_at TIMESTAMPTZ NOT NULL,
|
|
17264
|
+
ended_at TIMESTAMPTZ
|
|
17265
|
+
);
|
|
17266
|
+
CREATE INDEX IF NOT EXISTS idx_twi_secret_ticket ON ticket_work_intervals(project_secret, ticket_number);
|
|
17267
|
+
CREATE INDEX IF NOT EXISTS idx_twi_open ON ticket_work_intervals(project_secret, ticket_number, ended_at);
|
|
16886
17268
|
`);
|
|
16887
17269
|
await migrateNoteIds(db);
|
|
16888
17270
|
}
|
|
@@ -16916,7 +17298,7 @@ var init_connection = __esm({
|
|
|
16916
17298
|
init_zod();
|
|
16917
17299
|
init_errorMessage();
|
|
16918
17300
|
init_pglite();
|
|
16919
|
-
SCHEMA_VERSION =
|
|
17301
|
+
SCHEMA_VERSION = 5;
|
|
16920
17302
|
RECOVERY_MARKER_FILENAME = ".db-recovery-marker.json";
|
|
16921
17303
|
PENDING_RECOVERY_FILENAME = ".db-pending-recovery.json";
|
|
16922
17304
|
MAX_DEFERRED_RECOVERY_ATTEMPTS = 3;
|
|
@@ -17011,20 +17393,27 @@ function getOrCreateState2(dataDir) {
|
|
|
17011
17393
|
}
|
|
17012
17394
|
return state;
|
|
17013
17395
|
}
|
|
17014
|
-
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
|
|
17018
|
-
|
|
17396
|
+
function withGlobalBackupLock(fn) {
|
|
17397
|
+
let settle;
|
|
17398
|
+
let fail;
|
|
17399
|
+
const out = new Promise((resolve11, reject2) => {
|
|
17400
|
+
settle = resolve11;
|
|
17401
|
+
fail = reject2;
|
|
17402
|
+
});
|
|
17403
|
+
void getBackgroundScheduler().submit({
|
|
17404
|
+
key: `backup:${++backupLockSeq}`,
|
|
17405
|
+
priority: PRIORITY.BACKUP,
|
|
17406
|
+
exclusiveGroup: BACKUP_EXCLUSIVE_GROUP,
|
|
17407
|
+
deferUnderLag: false,
|
|
17408
|
+
run: async () => {
|
|
17409
|
+
try {
|
|
17410
|
+
settle(await fn());
|
|
17411
|
+
} catch (e) {
|
|
17412
|
+
fail(e);
|
|
17413
|
+
}
|
|
17019
17414
|
}
|
|
17020
|
-
}
|
|
17021
|
-
|
|
17022
|
-
activeBackup = p;
|
|
17023
|
-
try {
|
|
17024
|
-
return await p;
|
|
17025
|
-
} finally {
|
|
17026
|
-
if (activeBackup === p) activeBackup = null;
|
|
17027
|
-
}
|
|
17415
|
+
});
|
|
17416
|
+
return out;
|
|
17028
17417
|
}
|
|
17029
17418
|
function backupsDir(dataDir) {
|
|
17030
17419
|
return getBackupDir(dataDir);
|
|
@@ -17288,12 +17677,18 @@ function initBackupScheduler(dataDir) {
|
|
|
17288
17677
|
});
|
|
17289
17678
|
}, 3e4);
|
|
17290
17679
|
state.attachmentGcInterval = setInterval(() => {
|
|
17291
|
-
void
|
|
17292
|
-
|
|
17293
|
-
|
|
17294
|
-
|
|
17295
|
-
|
|
17296
|
-
|
|
17680
|
+
void getBackgroundScheduler().submit({
|
|
17681
|
+
key: `attachment-gc:${dataDir}`,
|
|
17682
|
+
priority: PRIORITY.GC,
|
|
17683
|
+
projectKey: dataDir,
|
|
17684
|
+
deferUnderLag: true,
|
|
17685
|
+
run: () => instrumentAsync(dataDir, "attachmentBackup.orphanGc:daily", () => runAttachmentGc(backupsDir(dataDir))).then((stats) => {
|
|
17686
|
+
if (stats.deleted > 0) {
|
|
17687
|
+
console.log(`[attachmentBackup] GC: reclaimed ${stats.deleted} blob(s), ${(stats.bytesReclaimed / 1024 / 1024).toFixed(2)} MB`);
|
|
17688
|
+
}
|
|
17689
|
+
}).catch((err) => {
|
|
17690
|
+
console.error("[attachmentBackup] GC daily run failed:", err);
|
|
17691
|
+
})
|
|
17297
17692
|
});
|
|
17298
17693
|
}, 24 * 60 * 60 * 1e3);
|
|
17299
17694
|
}
|
|
@@ -17312,7 +17707,7 @@ async function triggerMissedBackups(dataDir) {
|
|
|
17312
17707
|
await createBackup(dataDir, tier);
|
|
17313
17708
|
}
|
|
17314
17709
|
}
|
|
17315
|
-
var TIERS, backupStates, activePreviews,
|
|
17710
|
+
var TIERS, backupStates, activePreviews, backupLockSeq, BACKUP_EXCLUSIVE_GROUP, VALID_TIERS;
|
|
17316
17711
|
var init_backup = __esm({
|
|
17317
17712
|
"src/backup.ts"() {
|
|
17318
17713
|
"use strict";
|
|
@@ -17323,6 +17718,7 @@ var init_backup = __esm({
|
|
|
17323
17718
|
init_dbJsonExport();
|
|
17324
17719
|
init_freezeLogger();
|
|
17325
17720
|
init_file_settings();
|
|
17721
|
+
init_backgroundScheduler();
|
|
17326
17722
|
TIERS = {
|
|
17327
17723
|
"5min": { intervalMs: 5 * 60 * 1e3, maxAge: 60 * 60 * 1e3, maxCount: 12 },
|
|
17328
17724
|
"hourly": { intervalMs: 60 * 60 * 1e3, maxAge: 12 * 60 * 60 * 1e3, maxCount: 12 },
|
|
@@ -17330,7 +17726,8 @@ var init_backup = __esm({
|
|
|
17330
17726
|
};
|
|
17331
17727
|
backupStates = /* @__PURE__ */ new Map();
|
|
17332
17728
|
activePreviews = /* @__PURE__ */ new Map();
|
|
17333
|
-
|
|
17729
|
+
backupLockSeq = 0;
|
|
17730
|
+
BACKUP_EXCLUSIVE_GROUP = "backup";
|
|
17334
17731
|
VALID_TIERS = /* @__PURE__ */ new Set(["5min", "hourly", "daily"]);
|
|
17335
17732
|
}
|
|
17336
17733
|
});
|
|
@@ -17905,6 +18302,47 @@ var init_ticketNumber = __esm({
|
|
|
17905
18302
|
}
|
|
17906
18303
|
});
|
|
17907
18304
|
|
|
18305
|
+
// src/db/ticketWorkIntervals.ts
|
|
18306
|
+
async function recordTicketWorkTransition(secret, ticketNumber, status) {
|
|
18307
|
+
if (secret === "" || ticketNumber === "") return;
|
|
18308
|
+
try {
|
|
18309
|
+
const db = await getTelemetryDb();
|
|
18310
|
+
await db.query(
|
|
18311
|
+
`UPDATE ticket_work_intervals SET ended_at = NOW()
|
|
18312
|
+
WHERE project_secret = $1 AND ticket_number = $2 AND ended_at IS NULL`,
|
|
18313
|
+
[secret, ticketNumber]
|
|
18314
|
+
);
|
|
18315
|
+
if (status === "started") {
|
|
18316
|
+
await db.query(
|
|
18317
|
+
`INSERT INTO ticket_work_intervals (project_secret, ticket_number, started_at)
|
|
18318
|
+
VALUES ($1, $2, NOW())`,
|
|
18319
|
+
[secret, ticketNumber]
|
|
18320
|
+
);
|
|
18321
|
+
}
|
|
18322
|
+
} catch (err) {
|
|
18323
|
+
console.warn("[ticketWorkIntervals] failed to record transition:", err instanceof Error ? err.message : String(err));
|
|
18324
|
+
}
|
|
18325
|
+
}
|
|
18326
|
+
async function closeOpenTicketIntervalsForProject(secret) {
|
|
18327
|
+
if (secret === "") return;
|
|
18328
|
+
try {
|
|
18329
|
+
const db = await getTelemetryDb();
|
|
18330
|
+
await db.query(
|
|
18331
|
+
`UPDATE ticket_work_intervals SET ended_at = NOW()
|
|
18332
|
+
WHERE project_secret = $1 AND ended_at IS NULL`,
|
|
18333
|
+
[secret]
|
|
18334
|
+
);
|
|
18335
|
+
} catch (err) {
|
|
18336
|
+
console.warn("[ticketWorkIntervals] failed to close open intervals:", err instanceof Error ? err.message : String(err));
|
|
18337
|
+
}
|
|
18338
|
+
}
|
|
18339
|
+
var init_ticketWorkIntervals = __esm({
|
|
18340
|
+
"src/db/ticketWorkIntervals.ts"() {
|
|
18341
|
+
"use strict";
|
|
18342
|
+
init_connection();
|
|
18343
|
+
}
|
|
18344
|
+
});
|
|
18345
|
+
|
|
17908
18346
|
// src/db/tickets.ts
|
|
17909
18347
|
function escapeIlike(value) {
|
|
17910
18348
|
return value.replace(/[%_\\]/g, "\\$&");
|
|
@@ -18037,7 +18475,17 @@ async function updateTicket(id, updates, options) {
|
|
|
18037
18475
|
`UPDATE tickets SET ${sets.join(", ")} WHERE id = $${paramIdx} RETURNING *`,
|
|
18038
18476
|
values
|
|
18039
18477
|
);
|
|
18040
|
-
|
|
18478
|
+
const updated = result.rows[0] ?? null;
|
|
18479
|
+
if (updates.status !== void 0) {
|
|
18480
|
+
try {
|
|
18481
|
+
const secret = readFileSettings(getDataDir()).secret;
|
|
18482
|
+
if (secret !== void 0 && secret !== "") {
|
|
18483
|
+
void recordTicketWorkTransition(secret, updated.ticket_number, updates.status);
|
|
18484
|
+
}
|
|
18485
|
+
} catch {
|
|
18486
|
+
}
|
|
18487
|
+
}
|
|
18488
|
+
return updated;
|
|
18041
18489
|
}
|
|
18042
18490
|
async function deleteTicket(id) {
|
|
18043
18491
|
await updateTicket(id, { status: "deleted" });
|
|
@@ -18376,9 +18824,11 @@ var QUERYABLE_FIELDS, PRIORITY_ORD, STATUS_ORD, PRIORITY_RANK, STATUS_RANK;
|
|
|
18376
18824
|
var init_tickets = __esm({
|
|
18377
18825
|
"src/db/tickets.ts"() {
|
|
18378
18826
|
"use strict";
|
|
18827
|
+
init_file_settings();
|
|
18379
18828
|
init_ticketNumber();
|
|
18380
18829
|
init_connection();
|
|
18381
18830
|
init_notes();
|
|
18831
|
+
init_ticketWorkIntervals();
|
|
18382
18832
|
QUERYABLE_FIELDS = /* @__PURE__ */ new Set(["category", "priority", "status", "title", "details", "up_next", "tags"]);
|
|
18383
18833
|
PRIORITY_ORD = `CASE priority WHEN 'highest' THEN 1 WHEN 'high' THEN 2 WHEN 'default' THEN 3 WHEN 'low' THEN 4 WHEN 'lowest' THEN 5 ELSE 3 END`;
|
|
18384
18834
|
STATUS_ORD = `CASE status WHEN 'backlog' THEN 1 WHEN 'not_started' THEN 2 WHEN 'started' THEN 3 WHEN 'completed' THEN 4 WHEN 'verified' THEN 5 WHEN 'archive' THEN 6 ELSE 2 END`;
|
|
@@ -19483,7 +19933,14 @@ function scheduleWorklistSync(dir) {
|
|
|
19483
19933
|
if (!state) return;
|
|
19484
19934
|
if (state.worklistTimeout) clearTimeout(state.worklistTimeout);
|
|
19485
19935
|
state.worklistTimeout = setTimeout(() => {
|
|
19486
|
-
|
|
19936
|
+
state.worklistTimeout = null;
|
|
19937
|
+
void getBackgroundScheduler().submit({
|
|
19938
|
+
key: `markdown-worklist:${state.dataDir}`,
|
|
19939
|
+
priority: PRIORITY.MARKDOWN_SYNC,
|
|
19940
|
+
projectKey: state.dataDir,
|
|
19941
|
+
deferUnderLag: true,
|
|
19942
|
+
run: () => runWithDataDir(state.dataDir, () => instrumentAsync(state.dataDir, "markdown.syncWorklist", () => syncWorklist(state)))
|
|
19943
|
+
});
|
|
19487
19944
|
}, WORKLIST_SYNC_DEBOUNCE_MS);
|
|
19488
19945
|
}
|
|
19489
19946
|
function scheduleOpenTicketsSync(dir) {
|
|
@@ -19491,7 +19948,14 @@ function scheduleOpenTicketsSync(dir) {
|
|
|
19491
19948
|
if (!state) return;
|
|
19492
19949
|
if (state.openTicketsTimeout) clearTimeout(state.openTicketsTimeout);
|
|
19493
19950
|
state.openTicketsTimeout = setTimeout(() => {
|
|
19494
|
-
|
|
19951
|
+
state.openTicketsTimeout = null;
|
|
19952
|
+
void getBackgroundScheduler().submit({
|
|
19953
|
+
key: `markdown-opentickets:${state.dataDir}`,
|
|
19954
|
+
priority: PRIORITY.MARKDOWN_SYNC,
|
|
19955
|
+
projectKey: state.dataDir,
|
|
19956
|
+
deferUnderLag: true,
|
|
19957
|
+
run: () => runWithDataDir(state.dataDir, () => instrumentAsync(state.dataDir, "markdown.syncOpenTickets", () => syncOpenTickets(state)))
|
|
19958
|
+
});
|
|
19495
19959
|
}, OPEN_TICKETS_SYNC_DEBOUNCE_MS);
|
|
19496
19960
|
}
|
|
19497
19961
|
function scheduleAllSync(dir) {
|
|
@@ -19809,6 +20273,7 @@ var init_markdown = __esm({
|
|
|
19809
20273
|
init_freezeLogger();
|
|
19810
20274
|
init_file_settings();
|
|
19811
20275
|
init_limits();
|
|
20276
|
+
init_backgroundScheduler();
|
|
19812
20277
|
init_schemas3();
|
|
19813
20278
|
syncStates = /* @__PURE__ */ new Map();
|
|
19814
20279
|
defaultDataDir2 = null;
|
|
@@ -19876,7 +20341,10 @@ async function registerProject(dataDir, port) {
|
|
|
19876
20341
|
const existing = projects.get(existingSecret);
|
|
19877
20342
|
if (existing) return existing;
|
|
19878
20343
|
}
|
|
20344
|
+
const dbT0 = Date.now();
|
|
19879
20345
|
const db = await getDbForDir(absDataDir);
|
|
20346
|
+
const dbMs = Date.now() - dbT0;
|
|
20347
|
+
if (dbMs > 500) startupLog(`[restore-step] getDbForDir took ${String(dbMs)}ms for ${absDataDir}`);
|
|
19880
20348
|
const { migrateDbSettingsToFile: migrateDbSettingsToFile2 } = await Promise.resolve().then(() => (init_migrate_settings(), migrate_settings_exports));
|
|
19881
20349
|
await runWithDataDir(absDataDir, () => migrateDbSettingsToFile2(absDataDir));
|
|
19882
20350
|
const secret = ensureSecret(absDataDir, port);
|
|
@@ -19986,6 +20454,7 @@ var init_projects = __esm({
|
|
|
19986
20454
|
init_file_settings();
|
|
19987
20455
|
init_lock();
|
|
19988
20456
|
init_skills();
|
|
20457
|
+
init_startup_log();
|
|
19989
20458
|
init_markdown();
|
|
19990
20459
|
init_isExecutableOnPath();
|
|
19991
20460
|
projects = /* @__PURE__ */ new Map();
|
|
@@ -21365,6 +21834,25 @@ var init_sessionStore = __esm({
|
|
|
21365
21834
|
}
|
|
21366
21835
|
});
|
|
21367
21836
|
|
|
21837
|
+
// src/activeProjects.ts
|
|
21838
|
+
function markProjectActive(dataDir) {
|
|
21839
|
+
lastActiveAt.set(dataDir, Date.now());
|
|
21840
|
+
}
|
|
21841
|
+
function isProjectActive(dataDir) {
|
|
21842
|
+
if (lastActiveAt.size === 0) return true;
|
|
21843
|
+
const at = lastActiveAt.get(dataDir);
|
|
21844
|
+
if (at === void 0) return false;
|
|
21845
|
+
return Date.now() - at < ACTIVE_TTL_MS;
|
|
21846
|
+
}
|
|
21847
|
+
var ACTIVE_TTL_MS, lastActiveAt;
|
|
21848
|
+
var init_activeProjects = __esm({
|
|
21849
|
+
"src/activeProjects.ts"() {
|
|
21850
|
+
"use strict";
|
|
21851
|
+
ACTIVE_TTL_MS = 9e4;
|
|
21852
|
+
lastActiveAt = /* @__PURE__ */ new Map();
|
|
21853
|
+
}
|
|
21854
|
+
});
|
|
21855
|
+
|
|
21368
21856
|
// src/gitignore.ts
|
|
21369
21857
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
21370
21858
|
import { appendFileSync as appendFileSync3, existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
|
|
@@ -21420,56 +21908,72 @@ var init_gitignore = __esm({
|
|
|
21420
21908
|
});
|
|
21421
21909
|
|
|
21422
21910
|
// src/git/status.ts
|
|
21423
|
-
import {
|
|
21911
|
+
import { execFile as execFile3 } from "child_process";
|
|
21424
21912
|
import { join as join23 } from "path";
|
|
21913
|
+
import { promisify as promisify2 } from "util";
|
|
21914
|
+
function bufToStr(v) {
|
|
21915
|
+
if (typeof v === "string") return v;
|
|
21916
|
+
if (v !== void 0) return v.toString();
|
|
21917
|
+
return "";
|
|
21918
|
+
}
|
|
21425
21919
|
function makeGitInvoker({ timeoutMs, includeStderr = false }) {
|
|
21426
|
-
return (args, cwd) => {
|
|
21427
|
-
const
|
|
21920
|
+
return async (args, cwd) => {
|
|
21921
|
+
const opts = {
|
|
21428
21922
|
cwd,
|
|
21429
21923
|
encoding: "utf-8",
|
|
21430
21924
|
timeout: timeoutMs,
|
|
21925
|
+
maxBuffer: GIT_MAX_BUFFER,
|
|
21431
21926
|
env: {
|
|
21432
21927
|
...process.env,
|
|
21433
21928
|
GIT_TERMINAL_PROMPT: "0",
|
|
21434
21929
|
GIT_OPTIONAL_LOCKS: "0"
|
|
21435
21930
|
}
|
|
21436
|
-
}
|
|
21437
|
-
|
|
21438
|
-
|
|
21439
|
-
|
|
21931
|
+
};
|
|
21932
|
+
try {
|
|
21933
|
+
const { stdout, stderr } = await execFileAsync2("git", args, opts);
|
|
21934
|
+
const out = bufToStr(stdout);
|
|
21935
|
+
const err = bufToStr(stderr);
|
|
21936
|
+
return { stdout: includeStderr ? out + err : out, status: 0 };
|
|
21937
|
+
} catch (e) {
|
|
21938
|
+
const errObj = e;
|
|
21939
|
+
const out = bufToStr(errObj.stdout);
|
|
21940
|
+
const err = bufToStr(errObj.stderr);
|
|
21941
|
+
const status = typeof errObj.code === "number" ? errObj.code : null;
|
|
21942
|
+
return { stdout: includeStderr ? out + err : out, status };
|
|
21943
|
+
}
|
|
21440
21944
|
};
|
|
21441
21945
|
}
|
|
21442
|
-
function getGitStatus(projectRoot3, invoker = defaultInvoker) {
|
|
21946
|
+
async function getGitStatus(projectRoot3, invoker = defaultInvoker) {
|
|
21443
21947
|
if (!isGitRepo(projectRoot3)) return null;
|
|
21444
|
-
return
|
|
21948
|
+
return instrumentAsync(join23(projectRoot3, ".hotsheet"), "git.getStatus", () => getGitStatusUnwrapped(projectRoot3, invoker));
|
|
21445
21949
|
}
|
|
21446
|
-
function getGitStatusUnwrapped(projectRoot3, invoker) {
|
|
21950
|
+
async function getGitStatusUnwrapped(projectRoot3, invoker) {
|
|
21447
21951
|
const root2 = getGitRoot(projectRoot3) ?? projectRoot3;
|
|
21448
|
-
const branchRes = invoker(["symbolic-ref", "--short", "HEAD"], root2);
|
|
21952
|
+
const branchRes = await invoker(["symbolic-ref", "--short", "HEAD"], root2);
|
|
21449
21953
|
let branch;
|
|
21450
21954
|
let detached = false;
|
|
21451
21955
|
if (branchRes.status === 0 && branchRes.stdout.trim() !== "") {
|
|
21452
21956
|
branch = branchRes.stdout.trim();
|
|
21453
21957
|
} else {
|
|
21454
21958
|
detached = true;
|
|
21455
|
-
const sha = invoker(["rev-parse", "--short", "HEAD"], root2);
|
|
21959
|
+
const sha = await invoker(["rev-parse", "--short", "HEAD"], root2);
|
|
21456
21960
|
branch = sha.status === 0 && sha.stdout.trim() !== "" ? sha.stdout.trim() : "(detached)";
|
|
21457
21961
|
}
|
|
21458
|
-
const porcelain = invoker(["status", "--porcelain=v1", "--no-renames"], root2);
|
|
21962
|
+
const porcelain = await invoker(["status", "--porcelain=v1", "--no-renames"], root2);
|
|
21459
21963
|
const counts = porcelain.status === 0 ? bucketPorcelain(porcelain.stdout) : { staged: 0, unstaged: 0, untracked: 0, conflicted: 0 };
|
|
21460
21964
|
let upstream = null;
|
|
21461
21965
|
let ahead = 0;
|
|
21462
21966
|
let behind = 0;
|
|
21463
21967
|
if (!detached) {
|
|
21464
|
-
const upRes = invoker(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root2);
|
|
21968
|
+
const upRes = await invoker(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root2);
|
|
21465
21969
|
if (upRes.status === 0 && upRes.stdout.trim() !== "") {
|
|
21466
21970
|
upstream = upRes.stdout.trim();
|
|
21467
|
-
const aheadRes = invoker(["rev-list", "--count", "@{u}..HEAD"], root2);
|
|
21971
|
+
const aheadRes = await invoker(["rev-list", "--count", "@{u}..HEAD"], root2);
|
|
21468
21972
|
if (aheadRes.status === 0) {
|
|
21469
21973
|
const n = Number.parseInt(aheadRes.stdout.trim(), 10);
|
|
21470
21974
|
if (Number.isFinite(n)) ahead = n;
|
|
21471
21975
|
}
|
|
21472
|
-
const behindRes = invoker(["rev-list", "--count", "HEAD..@{u}"], root2);
|
|
21976
|
+
const behindRes = await invoker(["rev-list", "--count", "HEAD..@{u}"], root2);
|
|
21473
21977
|
if (behindRes.status === 0) {
|
|
21474
21978
|
const n = Number.parseInt(behindRes.stdout.trim(), 10);
|
|
21475
21979
|
if (Number.isFinite(n)) behind = n;
|
|
@@ -21492,16 +21996,16 @@ function getGitStatusUnwrapped(projectRoot3, invoker) {
|
|
|
21492
21996
|
function getLastFetchedAt(projectRoot3) {
|
|
21493
21997
|
return lastFetchedAt.get(projectRoot3) ?? null;
|
|
21494
21998
|
}
|
|
21495
|
-
function runGitFetch(projectRoot3, invoker = makeGitInvoker({ timeoutMs: 3e4, includeStderr: true })) {
|
|
21999
|
+
async function runGitFetch(projectRoot3, invoker = makeGitInvoker({ timeoutMs: 3e4, includeStderr: true })) {
|
|
21496
22000
|
if (!isGitRepo(projectRoot3)) {
|
|
21497
22001
|
return { ok: false, lastFetchedAt: null, error: "Not a git repository" };
|
|
21498
22002
|
}
|
|
21499
22003
|
const root2 = getGitRoot(projectRoot3) ?? projectRoot3;
|
|
21500
|
-
const upRes = invoker(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root2);
|
|
22004
|
+
const upRes = await invoker(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], root2);
|
|
21501
22005
|
if (upRes.status !== 0 || upRes.stdout.trim() === "") {
|
|
21502
22006
|
return { ok: false, lastFetchedAt: null, error: "No upstream branch \u2014 set one with `git push -u <remote> <branch>`." };
|
|
21503
22007
|
}
|
|
21504
|
-
const fetchRes = invoker(["fetch", "--quiet", "--no-write-fetch-head"], root2);
|
|
22008
|
+
const fetchRes = await invoker(["fetch", "--quiet", "--no-write-fetch-head"], root2);
|
|
21505
22009
|
if (fetchRes.status === 0) {
|
|
21506
22010
|
const now = Date.now();
|
|
21507
22011
|
lastFetchedAt.set(projectRoot3, now);
|
|
@@ -21528,11 +22032,11 @@ function bucketPorcelain(output) {
|
|
|
21528
22032
|
}
|
|
21529
22033
|
return out;
|
|
21530
22034
|
}
|
|
21531
|
-
function getGitStatusFiles(projectRoot3, invoker = defaultInvoker) {
|
|
22035
|
+
async function getGitStatusFiles(projectRoot3, invoker = defaultInvoker) {
|
|
21532
22036
|
if (!isGitRepo(projectRoot3)) return null;
|
|
21533
|
-
return
|
|
22037
|
+
return instrumentAsync(join23(projectRoot3, ".hotsheet"), "git.getStatusFiles", async () => {
|
|
21534
22038
|
const root2 = getGitRoot(projectRoot3) ?? projectRoot3;
|
|
21535
|
-
const res = invoker(["status", "--porcelain=v1", "--no-renames", "-z"], root2);
|
|
22039
|
+
const res = await invoker(["status", "--porcelain=v1", "--no-renames", "-z"], root2);
|
|
21536
22040
|
if (res.status !== 0) return null;
|
|
21537
22041
|
return bucketPorcelainFiles(res.stdout);
|
|
21538
22042
|
});
|
|
@@ -21579,13 +22083,15 @@ function pushCapped(arr, item, onTruncate) {
|
|
|
21579
22083
|
}
|
|
21580
22084
|
arr.push(item);
|
|
21581
22085
|
}
|
|
21582
|
-
var SPAWN_TIMEOUT_MS, defaultInvoker, lastFetchedAt, FILES_PER_BUCKET_CAP;
|
|
22086
|
+
var execFileAsync2, SPAWN_TIMEOUT_MS, GIT_MAX_BUFFER, defaultInvoker, lastFetchedAt, FILES_PER_BUCKET_CAP;
|
|
21583
22087
|
var init_status = __esm({
|
|
21584
22088
|
"src/git/status.ts"() {
|
|
21585
22089
|
"use strict";
|
|
21586
22090
|
init_freezeLogger();
|
|
21587
22091
|
init_gitignore();
|
|
22092
|
+
execFileAsync2 = promisify2(execFile3);
|
|
21588
22093
|
SPAWN_TIMEOUT_MS = 2e3;
|
|
22094
|
+
GIT_MAX_BUFFER = 32 * 1024 * 1024;
|
|
21589
22095
|
defaultInvoker = makeGitInvoker({ timeoutMs: SPAWN_TIMEOUT_MS });
|
|
21590
22096
|
lastFetchedAt = /* @__PURE__ */ new Map();
|
|
21591
22097
|
FILES_PER_BUCKET_CAP = 200;
|
|
@@ -21606,21 +22112,33 @@ __export(watcher_exports, {
|
|
|
21606
22112
|
});
|
|
21607
22113
|
import { existsSync as existsSync17, watch as fsWatch } from "fs";
|
|
21608
22114
|
import { join as join24 } from "path";
|
|
21609
|
-
function getCachedGitStatus(projectRoot3) {
|
|
22115
|
+
async function getCachedGitStatus(projectRoot3) {
|
|
21610
22116
|
const entry = cache.get(projectRoot3);
|
|
21611
22117
|
const now = Date.now();
|
|
21612
22118
|
if (entry !== void 0 && now - entry.resolvedAt < CACHE_TTL_MS) {
|
|
21613
22119
|
return entry.status;
|
|
21614
22120
|
}
|
|
21615
|
-
const
|
|
21616
|
-
|
|
21617
|
-
|
|
22121
|
+
const pending2 = inFlight.get(projectRoot3);
|
|
22122
|
+
if (pending2 !== void 0) return pending2;
|
|
22123
|
+
const p = (async () => {
|
|
22124
|
+
try {
|
|
22125
|
+
const status = await getGitStatus(projectRoot3);
|
|
22126
|
+
cache.set(projectRoot3, { status, resolvedAt: Date.now() });
|
|
22127
|
+
return status;
|
|
22128
|
+
} finally {
|
|
22129
|
+
inFlight.delete(projectRoot3);
|
|
22130
|
+
}
|
|
22131
|
+
})();
|
|
22132
|
+
inFlight.set(projectRoot3, p);
|
|
22133
|
+
return p;
|
|
21618
22134
|
}
|
|
21619
22135
|
function _resetGitStatusCacheForTests() {
|
|
21620
22136
|
cache.clear();
|
|
22137
|
+
inFlight.clear();
|
|
21621
22138
|
}
|
|
21622
22139
|
function dropGitStatusCache(projectRoot3) {
|
|
21623
22140
|
cache.delete(projectRoot3);
|
|
22141
|
+
inFlight.delete(projectRoot3);
|
|
21624
22142
|
}
|
|
21625
22143
|
function subscribeToGitChanges(handler) {
|
|
21626
22144
|
subscribers.add(handler);
|
|
@@ -21649,13 +22167,22 @@ function ensureGitWatcher(projectRoot3) {
|
|
|
21649
22167
|
if (e2 === void 0) return;
|
|
21650
22168
|
e2.debounce = null;
|
|
21651
22169
|
cache.delete(projectRoot3);
|
|
22170
|
+
inFlight.delete(projectRoot3);
|
|
21652
22171
|
e2.version++;
|
|
22172
|
+
if (!isProjectActive(join24(projectRoot3, ".hotsheet"))) return;
|
|
21653
22173
|
for (const sub of subscribers) {
|
|
21654
22174
|
try {
|
|
21655
22175
|
sub(projectRoot3);
|
|
21656
22176
|
} catch {
|
|
21657
22177
|
}
|
|
21658
22178
|
}
|
|
22179
|
+
void getBackgroundScheduler().submit({
|
|
22180
|
+
key: `git-refresh:${projectRoot3}`,
|
|
22181
|
+
priority: PRIORITY.GIT_STATUS,
|
|
22182
|
+
projectKey: projectRoot3,
|
|
22183
|
+
deferUnderLag: true,
|
|
22184
|
+
run: () => getCachedGitStatus(projectRoot3).then(() => void 0)
|
|
22185
|
+
});
|
|
21659
22186
|
}, WATCHER_DEBOUNCE_MS);
|
|
21660
22187
|
};
|
|
21661
22188
|
for (const file2 of filenames) {
|
|
@@ -21682,19 +22209,23 @@ function disposeGitWatcher(projectRoot3) {
|
|
|
21682
22209
|
watchers.delete(projectRoot3);
|
|
21683
22210
|
}
|
|
21684
22211
|
cache.delete(projectRoot3);
|
|
22212
|
+
inFlight.delete(projectRoot3);
|
|
21685
22213
|
}
|
|
21686
22214
|
function disposeAllGitWatchers() {
|
|
21687
22215
|
for (const root2 of [...watchers.keys()]) disposeGitWatcher(root2);
|
|
21688
22216
|
subscribers.clear();
|
|
21689
22217
|
}
|
|
21690
|
-
var CACHE_TTL_MS, cache, WATCHER_DEBOUNCE_MS, watchers, subscribers;
|
|
22218
|
+
var CACHE_TTL_MS, cache, inFlight, WATCHER_DEBOUNCE_MS, watchers, subscribers;
|
|
21691
22219
|
var init_watcher = __esm({
|
|
21692
22220
|
"src/git/watcher.ts"() {
|
|
21693
22221
|
"use strict";
|
|
22222
|
+
init_activeProjects();
|
|
21694
22223
|
init_gitignore();
|
|
22224
|
+
init_backgroundScheduler();
|
|
21695
22225
|
init_status();
|
|
21696
22226
|
CACHE_TTL_MS = 500;
|
|
21697
22227
|
cache = /* @__PURE__ */ new Map();
|
|
22228
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
21698
22229
|
WATCHER_DEBOUNCE_MS = 250;
|
|
21699
22230
|
watchers = /* @__PURE__ */ new Map();
|
|
21700
22231
|
subscribers = /* @__PURE__ */ new Set();
|
|
@@ -23775,6 +24306,7 @@ async function runShutdownPipeline(reason) {
|
|
|
23775
24306
|
await killShellCommands();
|
|
23776
24307
|
await destroyTerminals();
|
|
23777
24308
|
await disposeGitWatchers();
|
|
24309
|
+
await terminateHashWorkerStep();
|
|
23778
24310
|
await snapshotDatabases();
|
|
23779
24311
|
await closeDatabases();
|
|
23780
24312
|
stopFreezeHeartbeat();
|
|
@@ -23807,6 +24339,14 @@ async function disposeGitWatchers() {
|
|
|
23807
24339
|
console.error("[lifecycle] disposeAllGitWatchers error:", err);
|
|
23808
24340
|
}
|
|
23809
24341
|
}
|
|
24342
|
+
async function terminateHashWorkerStep() {
|
|
24343
|
+
try {
|
|
24344
|
+
const { terminateHashWorker: terminateHashWorker2 } = await Promise.resolve().then(() => (init_hashWorker(), hashWorker_exports));
|
|
24345
|
+
await terminateHashWorker2();
|
|
24346
|
+
} catch (err) {
|
|
24347
|
+
console.error("[lifecycle] terminateHashWorker error:", err);
|
|
24348
|
+
}
|
|
24349
|
+
}
|
|
23810
24350
|
async function closeHttpServer() {
|
|
23811
24351
|
if (httpServer === null) return;
|
|
23812
24352
|
await new Promise((resolve11) => {
|
|
@@ -23901,25 +24441,25 @@ var init_mime_types = __esm({
|
|
|
23901
24441
|
// src/open-in-file-manager.ts
|
|
23902
24442
|
import { dirname as dirname6 } from "path";
|
|
23903
24443
|
async function openInFileManager(dirPath) {
|
|
23904
|
-
const { execFile:
|
|
24444
|
+
const { execFile: execFile8 } = await import("child_process");
|
|
23905
24445
|
const platform2 = process.platform;
|
|
23906
24446
|
if (platform2 === "darwin") {
|
|
23907
|
-
|
|
24447
|
+
execFile8("open", [dirPath]);
|
|
23908
24448
|
} else if (platform2 === "win32") {
|
|
23909
|
-
|
|
24449
|
+
execFile8("explorer", [dirPath]);
|
|
23910
24450
|
} else {
|
|
23911
|
-
|
|
24451
|
+
execFile8("xdg-open", [dirPath]);
|
|
23912
24452
|
}
|
|
23913
24453
|
}
|
|
23914
24454
|
async function revealInFileManager(filePath) {
|
|
23915
|
-
const { execFile:
|
|
24455
|
+
const { execFile: execFile8 } = await import("child_process");
|
|
23916
24456
|
const platform2 = process.platform;
|
|
23917
24457
|
if (platform2 === "darwin") {
|
|
23918
|
-
|
|
24458
|
+
execFile8("open", ["-R", filePath]);
|
|
23919
24459
|
} else if (platform2 === "win32") {
|
|
23920
|
-
|
|
24460
|
+
execFile8("explorer", ["/select,", filePath]);
|
|
23921
24461
|
} else {
|
|
23922
|
-
|
|
24462
|
+
execFile8("xdg-open", [dirname6(filePath)]);
|
|
23923
24463
|
}
|
|
23924
24464
|
}
|
|
23925
24465
|
var init_open_in_file_manager = __esm({
|
|
@@ -24217,14 +24757,14 @@ __export(keychain_exports, {
|
|
|
24217
24757
|
keychainGet: () => keychainGet,
|
|
24218
24758
|
keychainSet: () => keychainSet
|
|
24219
24759
|
});
|
|
24220
|
-
import { execFile as
|
|
24760
|
+
import { execFile as execFile4 } from "child_process";
|
|
24221
24761
|
import { platform } from "os";
|
|
24222
24762
|
function makeService(pluginId) {
|
|
24223
24763
|
return `${SERVICE_PREFIX}.${pluginId}`;
|
|
24224
24764
|
}
|
|
24225
24765
|
function exec(cmd, args) {
|
|
24226
24766
|
return new Promise((resolve11) => {
|
|
24227
|
-
|
|
24767
|
+
execFile4(cmd, args, { timeout: 5e3 }, (error51, stdout) => {
|
|
24228
24768
|
resolve11({ stdout: stdout.trim(), exitCode: error51 ? error51.status ?? 1 : 0 });
|
|
24229
24769
|
});
|
|
24230
24770
|
});
|
|
@@ -24276,7 +24816,7 @@ async function linuxGet(service, account) {
|
|
|
24276
24816
|
}
|
|
24277
24817
|
async function linuxSet(service, account, password) {
|
|
24278
24818
|
return new Promise((resolve11) => {
|
|
24279
|
-
const proc =
|
|
24819
|
+
const proc = execFile4("secret-tool", [
|
|
24280
24820
|
"store",
|
|
24281
24821
|
"--label",
|
|
24282
24822
|
`Hot Sheet: ${account}`,
|
|
@@ -26014,13 +26554,13 @@ var require_aspromise = __commonJS({
|
|
|
26014
26554
|
"use strict";
|
|
26015
26555
|
module.exports = asPromise;
|
|
26016
26556
|
function asPromise(fn, ctx) {
|
|
26017
|
-
var params = new Array(arguments.length - 1), offset = 0, index = 2,
|
|
26557
|
+
var params = new Array(arguments.length - 1), offset = 0, index = 2, pending2 = true;
|
|
26018
26558
|
while (index < arguments.length)
|
|
26019
26559
|
params[offset++] = arguments[index++];
|
|
26020
26560
|
return new Promise(function executor(resolve11, reject2) {
|
|
26021
26561
|
params[offset] = function callback(err) {
|
|
26022
|
-
if (
|
|
26023
|
-
|
|
26562
|
+
if (pending2) {
|
|
26563
|
+
pending2 = false;
|
|
26024
26564
|
if (err)
|
|
26025
26565
|
reject2(err);
|
|
26026
26566
|
else {
|
|
@@ -26034,8 +26574,8 @@ var require_aspromise = __commonJS({
|
|
|
26034
26574
|
try {
|
|
26035
26575
|
fn.apply(ctx || null, params);
|
|
26036
26576
|
} catch (err) {
|
|
26037
|
-
if (
|
|
26038
|
-
|
|
26577
|
+
if (pending2) {
|
|
26578
|
+
pending2 = false;
|
|
26039
26579
|
reject2(err);
|
|
26040
26580
|
}
|
|
26041
26581
|
}
|
|
@@ -32822,7 +33362,7 @@ __export(processPriority_exports, {
|
|
|
32822
33362
|
bumpProcessPriorityBestEffort: () => bumpProcessPriorityBestEffort,
|
|
32823
33363
|
shouldBumpProcessPriority: () => shouldBumpProcessPriority
|
|
32824
33364
|
});
|
|
32825
|
-
import { spawnSync
|
|
33365
|
+
import { spawnSync } from "child_process";
|
|
32826
33366
|
function shouldBumpProcessPriority(platform2) {
|
|
32827
33367
|
return platform2 === "darwin";
|
|
32828
33368
|
}
|
|
@@ -32834,7 +33374,7 @@ function bumpProcessPriorityBestEffort() {
|
|
|
32834
33374
|
const args = buildTaskpolicyArgs(process.pid);
|
|
32835
33375
|
let result;
|
|
32836
33376
|
try {
|
|
32837
|
-
result =
|
|
33377
|
+
result = spawnSync("taskpolicy", args, { encoding: "utf8", timeout: 2e3 });
|
|
32838
33378
|
} catch (err) {
|
|
32839
33379
|
console.warn(`[priority] taskpolicy spawn failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
32840
33380
|
return false;
|
|
@@ -32861,7 +33401,7 @@ var init_processPriority = __esm({
|
|
|
32861
33401
|
|
|
32862
33402
|
// src/cli.ts
|
|
32863
33403
|
init_backup();
|
|
32864
|
-
import { execFile as
|
|
33404
|
+
import { execFile as execFile7 } from "child_process";
|
|
32865
33405
|
import { existsSync as existsSync30, mkdirSync as mkdirSync18, realpathSync } from "fs";
|
|
32866
33406
|
import { tmpdir as tmpdir3 } from "os";
|
|
32867
33407
|
import { join as join36, resolve as resolve10 } from "path";
|
|
@@ -33365,7 +33905,7 @@ init_lifecycle2();
|
|
|
33365
33905
|
init_mime_types();
|
|
33366
33906
|
init_projects();
|
|
33367
33907
|
import { serve } from "@hono/node-server";
|
|
33368
|
-
import { execFile as
|
|
33908
|
+
import { execFile as execFile6 } from "child_process";
|
|
33369
33909
|
import { existsSync as existsSync28, readFileSync as readFileSync22 } from "fs";
|
|
33370
33910
|
import { Hono as Hono19 } from "hono";
|
|
33371
33911
|
import { basename as basename5, dirname as dirname8, join as join34 } from "path";
|
|
@@ -33563,6 +34103,7 @@ function listAliveEntries(dataDir, isPidAlive3 = defaultIsPidAlive) {
|
|
|
33563
34103
|
init_claude_hooks();
|
|
33564
34104
|
init_commandLog();
|
|
33565
34105
|
init_settings();
|
|
34106
|
+
init_ticketWorkIntervals();
|
|
33566
34107
|
init_freezeLogger();
|
|
33567
34108
|
init_file_settings();
|
|
33568
34109
|
init_global_config();
|
|
@@ -33834,7 +34375,10 @@ channelRoutes.post("/channel/permission/dismiss", async (c) => {
|
|
|
33834
34375
|
});
|
|
33835
34376
|
channelRoutes.post("/channel/done", (_c) => {
|
|
33836
34377
|
const secret = _c.get("projectSecret");
|
|
33837
|
-
if (secret)
|
|
34378
|
+
if (secret) {
|
|
34379
|
+
channelDoneFlags.set(secret, true);
|
|
34380
|
+
void closeOpenTicketIntervalsForProject(secret);
|
|
34381
|
+
}
|
|
33838
34382
|
addLogEntry("done", "incoming", "Claude finished", "").catch(() => {
|
|
33839
34383
|
});
|
|
33840
34384
|
notifyChange();
|
|
@@ -33932,6 +34476,7 @@ commandLogRoutes.get("/command-log/count", async (c) => {
|
|
|
33932
34476
|
});
|
|
33933
34477
|
|
|
33934
34478
|
// src/routes/dashboard.ts
|
|
34479
|
+
init_activeProjects();
|
|
33935
34480
|
init_queries();
|
|
33936
34481
|
init_stats();
|
|
33937
34482
|
init_gitignore();
|
|
@@ -33947,6 +34492,7 @@ import { homedir as homedir6, tmpdir } from "os";
|
|
|
33947
34492
|
import { join as join29, relative as relative2, resolve as resolve8 } from "path";
|
|
33948
34493
|
var dashboardRoutes = new Hono5();
|
|
33949
34494
|
dashboardRoutes.get("/poll", async (c) => {
|
|
34495
|
+
markProjectActive(c.get("dataDir"));
|
|
33950
34496
|
const clientVersion = Math.max(0, parseInt(c.req.query("version") ?? "0", 10) || 0);
|
|
33951
34497
|
const changeVersion2 = getChangeVersion();
|
|
33952
34498
|
if (changeVersion2 > clientVersion) {
|
|
@@ -35014,12 +35560,12 @@ import { Hono as Hono13 } from "hono";
|
|
|
35014
35560
|
// src/db/repair.ts
|
|
35015
35561
|
init_backup();
|
|
35016
35562
|
init_pglite();
|
|
35017
|
-
import { execFile as
|
|
35563
|
+
import { execFile as execFile5 } from "child_process";
|
|
35018
35564
|
import { cpSync as cpSync2, existsSync as existsSync26, mkdirSync as mkdirSync16, readFileSync as readFileSync21, rmSync as rmSync10, writeFileSync as writeFileSync18 } from "fs";
|
|
35019
35565
|
import { tmpdir as tmpdir2 } from "os";
|
|
35020
35566
|
import { join as join32 } from "path";
|
|
35021
|
-
import { promisify as
|
|
35022
|
-
var execFileP =
|
|
35567
|
+
import { promisify as promisify3 } from "util";
|
|
35568
|
+
var execFileP = promisify3(execFile5);
|
|
35023
35569
|
async function findWorkingBackup(dataDir) {
|
|
35024
35570
|
const backups = listBackups(dataDir);
|
|
35025
35571
|
for (const backup of backups) {
|
|
@@ -35210,6 +35756,7 @@ dbRoutes.post("/repair/run-pg-resetwal", async (c) => {
|
|
|
35210
35756
|
});
|
|
35211
35757
|
|
|
35212
35758
|
// src/routes/git.ts
|
|
35759
|
+
init_activeProjects();
|
|
35213
35760
|
import { Hono as Hono14 } from "hono";
|
|
35214
35761
|
import { join as join33 } from "path";
|
|
35215
35762
|
|
|
@@ -35271,18 +35818,19 @@ init_watcher();
|
|
|
35271
35818
|
init_gitignore();
|
|
35272
35819
|
init_open_in_file_manager();
|
|
35273
35820
|
var gitRoutes = new Hono14();
|
|
35274
|
-
gitRoutes.get("/git/status", (c) => {
|
|
35821
|
+
gitRoutes.get("/git/status", async (c) => {
|
|
35275
35822
|
const dataDir = c.get("dataDir");
|
|
35823
|
+
markProjectActive(dataDir);
|
|
35276
35824
|
const projectRoot3 = projectRootFromDataDir(dataDir);
|
|
35277
35825
|
const settings = readFileSettings(dataDir);
|
|
35278
35826
|
if (settings.git_tracking_enabled === false) {
|
|
35279
35827
|
return c.json(null);
|
|
35280
35828
|
}
|
|
35281
35829
|
ensureGitWatcher(projectRoot3);
|
|
35282
|
-
const status = getCachedGitStatus(projectRoot3);
|
|
35830
|
+
const status = await getCachedGitStatus(projectRoot3);
|
|
35283
35831
|
if (status === null) return c.json(null);
|
|
35284
35832
|
if (c.req.query("files") === "true") {
|
|
35285
|
-
const files = getGitStatusFiles(projectRoot3);
|
|
35833
|
+
const files = await getGitStatusFiles(projectRoot3);
|
|
35286
35834
|
return c.json({ ...status, files });
|
|
35287
35835
|
}
|
|
35288
35836
|
return c.json(status);
|
|
@@ -35290,14 +35838,14 @@ gitRoutes.get("/git/status", (c) => {
|
|
|
35290
35838
|
function projectRootFromDataDir(dataDir) {
|
|
35291
35839
|
return dataDir.replace(/[\\/]\.hotsheet\/?$/, "");
|
|
35292
35840
|
}
|
|
35293
|
-
gitRoutes.post("/git/fetch", (c) => {
|
|
35841
|
+
gitRoutes.post("/git/fetch", async (c) => {
|
|
35294
35842
|
const dataDir = c.get("dataDir");
|
|
35295
35843
|
const projectRoot3 = projectRootFromDataDir(dataDir);
|
|
35296
35844
|
const settings = readFileSettings(dataDir);
|
|
35297
35845
|
if (settings.git_tracking_enabled === false) {
|
|
35298
35846
|
return c.json({ ok: false, lastFetchedAt: null, error: "git tracking disabled in settings" });
|
|
35299
35847
|
}
|
|
35300
|
-
const result = runGitFetch(projectRoot3);
|
|
35848
|
+
const result = await runGitFetch(projectRoot3);
|
|
35301
35849
|
if (result.ok) dropGitStatusCache(projectRoot3);
|
|
35302
35850
|
return c.json(result);
|
|
35303
35851
|
});
|
|
@@ -38246,54 +38794,62 @@ async function getPromptTimeline(promptId) {
|
|
|
38246
38794
|
spans
|
|
38247
38795
|
};
|
|
38248
38796
|
}
|
|
38249
|
-
async function getPerTicketRollup(ticketNumber) {
|
|
38797
|
+
async function getPerTicketRollup(ticketNumber, secret) {
|
|
38250
38798
|
const db = await getTelemetryDb();
|
|
38251
38799
|
const marker = `hotsheet:ticket=${ticketNumber}`;
|
|
38252
|
-
const
|
|
38253
|
-
|
|
38254
|
-
|
|
38255
|
-
|
|
38256
|
-
|
|
38257
|
-
|
|
38258
|
-
|
|
38259
|
-
|
|
38260
|
-
|
|
38261
|
-
|
|
38262
|
-
|
|
38263
|
-
|
|
38264
|
-
|
|
38265
|
-
|
|
38266
|
-
|
|
38267
|
-
|
|
38268
|
-
|
|
38269
|
-
|
|
38270
|
-
|
|
38271
|
-
|
|
38272
|
-
|
|
38273
|
-
|
|
38274
|
-
|
|
38275
|
-
|
|
38276
|
-
|
|
38277
|
-
|
|
38278
|
-
|
|
38279
|
-
|
|
38280
|
-
|
|
38281
|
-
|
|
38282
|
-
|
|
38283
|
-
|
|
38284
|
-
|
|
38285
|
-
|
|
38286
|
-
|
|
38287
|
-
|
|
38288
|
-
|
|
38289
|
-
|
|
38800
|
+
const secretParam = secret !== void 0 && secret !== "" ? secret : null;
|
|
38801
|
+
const result = await db.query(
|
|
38802
|
+
`WITH marker_prompts AS (
|
|
38803
|
+
SELECT DISTINCT prompt_id FROM otel_events
|
|
38804
|
+
WHERE ${eventNameMatchSql("event_name", "user_prompt")}
|
|
38805
|
+
AND prompt_id IS NOT NULL
|
|
38806
|
+
AND body_json::text LIKE $1
|
|
38807
|
+
),
|
|
38808
|
+
matched AS (
|
|
38809
|
+
SELECT
|
|
38810
|
+
e.prompt_id,
|
|
38811
|
+
e.ts,
|
|
38812
|
+
COALESCE(
|
|
38813
|
+
(e.attributes_json->>'cost')::numeric,
|
|
38814
|
+
(e.attributes_json->>'cost_usd')::numeric,
|
|
38815
|
+
0
|
|
38816
|
+
) AS cost,
|
|
38817
|
+
COALESCE(
|
|
38818
|
+
(e.attributes_json->>'tokens')::numeric,
|
|
38819
|
+
(e.attributes_json->>'total_tokens')::numeric,
|
|
38820
|
+
(e.attributes_json->>'input_tokens')::numeric + (e.attributes_json->>'output_tokens')::numeric,
|
|
38821
|
+
0
|
|
38822
|
+
) AS tokens
|
|
38823
|
+
FROM otel_events e
|
|
38824
|
+
WHERE ${eventNameMatchSql("e.event_name", "api_request")}
|
|
38825
|
+
AND (
|
|
38826
|
+
e.prompt_id IN (SELECT prompt_id FROM marker_prompts)
|
|
38827
|
+
OR (
|
|
38828
|
+
$2::text IS NOT NULL AND e.project_secret = $2 AND EXISTS (
|
|
38829
|
+
SELECT 1 FROM ticket_work_intervals i
|
|
38830
|
+
WHERE i.project_secret = $2 AND i.ticket_number = $3
|
|
38831
|
+
AND e.ts >= i.started_at AND e.ts <= COALESCE(i.ended_at, NOW())
|
|
38832
|
+
)
|
|
38833
|
+
)
|
|
38834
|
+
)
|
|
38835
|
+
)
|
|
38836
|
+
SELECT
|
|
38837
|
+
(SELECT COUNT(DISTINCT prompt_id) FROM matched) AS prompt_count,
|
|
38838
|
+
(SELECT COALESCE(SUM(cost), 0) FROM matched) AS total_cost,
|
|
38839
|
+
(SELECT COALESCE(SUM(tokens), 0) FROM matched) AS total_tokens,
|
|
38840
|
+
(SELECT COALESCE(SUM(dur), 0) FROM (
|
|
38841
|
+
SELECT EXTRACT(EPOCH FROM (MAX(ts) - MIN(ts))) AS dur
|
|
38842
|
+
FROM matched WHERE prompt_id IS NOT NULL GROUP BY prompt_id
|
|
38843
|
+
) per_prompt) AS total_seconds`,
|
|
38844
|
+
[`%${marker}%`, secretParam, ticketNumber]
|
|
38290
38845
|
);
|
|
38846
|
+
const row = result.rows[0];
|
|
38291
38847
|
return {
|
|
38292
38848
|
ticketNumber,
|
|
38293
|
-
promptCount:
|
|
38294
|
-
totalCost: Number(
|
|
38295
|
-
totalTokens: Number(
|
|
38296
|
-
totalDurationSeconds: Number(
|
|
38849
|
+
promptCount: Number(row.prompt_count ?? 0),
|
|
38850
|
+
totalCost: Number(row.total_cost ?? 0),
|
|
38851
|
+
totalTokens: Number(row.total_tokens ?? 0),
|
|
38852
|
+
totalDurationSeconds: Number(row.total_seconds ?? 0)
|
|
38297
38853
|
};
|
|
38298
38854
|
}
|
|
38299
38855
|
async function getDashboardPayload(window2, timezone = "UTC", allowedSecrets = null, now = /* @__PURE__ */ new Date()) {
|
|
@@ -38375,7 +38931,8 @@ telemetryRoutes.get("/telemetry/prompt/:id", async (c) => {
|
|
|
38375
38931
|
});
|
|
38376
38932
|
telemetryRoutes.get("/telemetry/ticket/:number", async (c) => {
|
|
38377
38933
|
const ticketNumber = c.req.param("number");
|
|
38378
|
-
const
|
|
38934
|
+
const secret = c.get("projectSecret");
|
|
38935
|
+
const rollup = await getPerTicketRollup(ticketNumber, secret);
|
|
38379
38936
|
return c.json(rollup);
|
|
38380
38937
|
});
|
|
38381
38938
|
telemetryRoutes.get("/telemetry/enabled-anywhere", (c) => {
|
|
@@ -38717,7 +39274,7 @@ async function startServer(port, dataDir, options) {
|
|
|
38717
39274
|
`);
|
|
38718
39275
|
if (options?.noOpen !== true) {
|
|
38719
39276
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
38720
|
-
|
|
39277
|
+
execFile6(openCmd, [url2]);
|
|
38721
39278
|
}
|
|
38722
39279
|
return actualPort;
|
|
38723
39280
|
}
|
|
@@ -38895,8 +39452,12 @@ async function startAndConfigure(port, dataDir, strictPort) {
|
|
|
38895
39452
|
const secret = ensureSecret(dataDir, actualPort);
|
|
38896
39453
|
const { bumpProcessPriorityBestEffort: bumpProcessPriorityBestEffort2 } = await Promise.resolve().then(() => (init_processPriority(), processPriority_exports));
|
|
38897
39454
|
bumpProcessPriorityBestEffort2();
|
|
38898
|
-
const { startServerEventLoopHeartbeat: startServerEventLoopHeartbeat2 } = await Promise.resolve().then(() => (init_freezeLogger(), freezeLogger_exports));
|
|
39455
|
+
const { startServerEventLoopHeartbeat: startServerEventLoopHeartbeat2, onServerWake: onServerWake2 } = await Promise.resolve().then(() => (init_freezeLogger(), freezeLogger_exports));
|
|
38899
39456
|
startServerEventLoopHeartbeat2(dataDir);
|
|
39457
|
+
const { getBackgroundScheduler: getBackgroundScheduler2 } = await Promise.resolve().then(() => (init_backgroundScheduler(), backgroundScheduler_exports));
|
|
39458
|
+
onServerWake2(() => {
|
|
39459
|
+
getBackgroundScheduler2().noteWake();
|
|
39460
|
+
});
|
|
38900
39461
|
initMarkdownSync(dataDir, actualPort);
|
|
38901
39462
|
scheduleAllSync(dataDir);
|
|
38902
39463
|
const { runWithDataDir: runWith } = await Promise.resolve().then(() => (init_connection(), connection_exports));
|
|
@@ -38927,8 +39488,11 @@ async function startAndConfigure(port, dataDir, strictPort) {
|
|
|
38927
39488
|
}
|
|
38928
39489
|
async function postStartup(dataDir, actualPort, demo, noOpen) {
|
|
38929
39490
|
if (demo === null) {
|
|
39491
|
+
startupMark("post-startup: init backup scheduler");
|
|
38930
39492
|
initBackupScheduler(dataDir);
|
|
39493
|
+
startupMark("post-startup: init snapshot scheduler");
|
|
38931
39494
|
initSnapshotScheduler(dataDir);
|
|
39495
|
+
startupMark("post-startup: add to project list");
|
|
38932
39496
|
addToProjectList(dataDir);
|
|
38933
39497
|
startupMark("post-startup: restoring previous projects");
|
|
38934
39498
|
await restorePreviousProjects(dataDir, actualPort);
|
|
@@ -38945,28 +39509,47 @@ async function postStartup(dataDir, actualPort, demo, noOpen) {
|
|
|
38945
39509
|
if (!noOpen) {
|
|
38946
39510
|
const url2 = `http://localhost:${actualPort}`;
|
|
38947
39511
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
38948
|
-
|
|
39512
|
+
execFile7(openCmd, [url2]);
|
|
38949
39513
|
}
|
|
38950
39514
|
}
|
|
38951
39515
|
async function restorePreviousProjects(dataDir, actualPort) {
|
|
38952
39516
|
const previousProjects = readProjectList();
|
|
38953
39517
|
const absDataDir = resolve10(dataDir);
|
|
38954
|
-
const validProjects = [];
|
|
38955
39518
|
const { eagerSpawnTerminals: eagerSpawnTerminals2 } = await Promise.resolve().then(() => (init_eagerSpawn(), eagerSpawn_exports));
|
|
38956
|
-
|
|
38957
|
-
|
|
38958
|
-
|
|
38959
|
-
|
|
38960
|
-
|
|
38961
|
-
if (
|
|
38962
|
-
|
|
38963
|
-
|
|
38964
|
-
|
|
38965
|
-
|
|
38966
|
-
|
|
38967
|
-
|
|
38968
|
-
|
|
38969
|
-
|
|
39519
|
+
const { getBackgroundScheduler: getBackgroundScheduler2, PRIORITY: PRIORITY2 } = await Promise.resolve().then(() => (init_backgroundScheduler(), backgroundScheduler_exports));
|
|
39520
|
+
const { notifyChange: notifyChange2 } = await Promise.resolve().then(() => (init_notify(), notify_exports));
|
|
39521
|
+
const scheduler = getBackgroundScheduler2();
|
|
39522
|
+
const registeredOk = /* @__PURE__ */ new Set();
|
|
39523
|
+
await Promise.all(previousProjects.map((prevDir) => {
|
|
39524
|
+
if (prevDir === absDataDir) return Promise.resolve();
|
|
39525
|
+
if (!existsSync30(prevDir)) return Promise.resolve();
|
|
39526
|
+
return scheduler.submit({
|
|
39527
|
+
key: `project-restore:${prevDir}`,
|
|
39528
|
+
projectKey: prevDir,
|
|
39529
|
+
priority: PRIORITY2.PROJECT_RESTORE,
|
|
39530
|
+
// deferUnderLag MUST be false: restore is user-visible work that has to
|
|
39531
|
+
// make progress. Deferring it under lag starves it — and each restore job
|
|
39532
|
+
// itself spikes lag (PGLite WASM init), so a true `deferUnderLag` makes
|
|
39533
|
+
// the whole restore crawl (observed: 403s for 8 projects).
|
|
39534
|
+
deferUnderLag: false,
|
|
39535
|
+
run: async () => {
|
|
39536
|
+
const t0 = Date.now();
|
|
39537
|
+
startupLog(`[restore-timing] START ${prevDir}`);
|
|
39538
|
+
try {
|
|
39539
|
+
const ctx = await registerProject(prevDir, actualPort);
|
|
39540
|
+
registeredOk.add(prevDir);
|
|
39541
|
+
eagerSpawnTerminals2(ctx.secret, prevDir);
|
|
39542
|
+
notifyChange2();
|
|
39543
|
+
startupLog(`[restore-timing] ${prevDir} registered in ${String(Date.now() - t0)}ms`);
|
|
39544
|
+
} catch (e) {
|
|
39545
|
+
console.warn(`[startup] Failed to restore project ${prevDir}: ${getErrorMessage(e)}`);
|
|
39546
|
+
}
|
|
39547
|
+
}
|
|
39548
|
+
});
|
|
39549
|
+
}));
|
|
39550
|
+
const validProjects = previousProjects.filter(
|
|
39551
|
+
(prevDir) => prevDir === absDataDir || registeredOk.has(prevDir)
|
|
39552
|
+
);
|
|
38970
39553
|
if (validProjects.length !== previousProjects.length) {
|
|
38971
39554
|
const { reorderProjectList: reorderProjectList2 } = await Promise.resolve().then(() => (init_project_list(), project_list_exports));
|
|
38972
39555
|
reorderProjectList2(validProjects);
|
|
@@ -38975,7 +39558,6 @@ async function restorePreviousProjects(dataDir, actualPort) {
|
|
|
38975
39558
|
const { getProjectByDataDir: getByDir, reorderProjects: reorder } = await Promise.resolve().then(() => (init_projects(), projects_exports));
|
|
38976
39559
|
const secrets = validProjects.map((dir) => getByDir(dir)?.secret).filter((s) => s !== void 0);
|
|
38977
39560
|
if (secrets.length > 1) reorder(secrets);
|
|
38978
|
-
const { notifyChange: notifyChange2 } = await Promise.resolve().then(() => (init_notify(), notify_exports));
|
|
38979
39561
|
notifyChange2();
|
|
38980
39562
|
}
|
|
38981
39563
|
}
|
|
@@ -39208,7 +39790,8 @@ if (isEntryPoint) {
|
|
|
39208
39790
|
export {
|
|
39209
39791
|
computeIsEntryPoint,
|
|
39210
39792
|
createSignalHandler,
|
|
39211
|
-
migrateGlobalConfig
|
|
39793
|
+
migrateGlobalConfig,
|
|
39794
|
+
restorePreviousProjects
|
|
39212
39795
|
};
|
|
39213
39796
|
/*! Bundled license information:
|
|
39214
39797
|
|