dsh-sessions-manager 3.4.1 → 3.4.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/lib/index.js +193 -24
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/index.js +80 -8
- package/src/title-persist-index.js +137 -0
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.js
|
|
2
|
-
import { mkdir as
|
|
3
|
-
import { basename, dirname, isAbsolute, join as
|
|
2
|
+
import { mkdir as mkdir4, realpath, rename as rename4, stat, unlink, writeFile as writeFile4 } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname as dirname2, isAbsolute, join as join4 } from "node:path";
|
|
4
4
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
5
5
|
import { homedir as homedir3 } from "node:os";
|
|
6
6
|
|
|
@@ -500,12 +500,116 @@ function createSessionMetaCache(opts = {}) {
|
|
|
500
500
|
};
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
// src/title-persist-index.js
|
|
504
|
+
import { mkdir as mkdir3, readFile, rename as rename3, writeFile as writeFile3 } from "node:fs/promises";
|
|
505
|
+
import { dirname, join as join3 } from "node:path";
|
|
506
|
+
var TITLE_INDEX_SCHEMA_VERSION = 1;
|
|
507
|
+
var MAX_ENTRIES = 2e4;
|
|
508
|
+
function normalizeEntry(raw) {
|
|
509
|
+
if (!raw || typeof raw !== "object") return null;
|
|
510
|
+
const title = typeof raw.title === "string" ? raw.title : null;
|
|
511
|
+
const cwd = typeof raw.cwd === "string" ? raw.cwd : null;
|
|
512
|
+
const createdAt = typeof raw.createdAt === "number" ? raw.createdAt : null;
|
|
513
|
+
const fingerprint = typeof raw.fingerprint === "string" && raw.fingerprint ? raw.fingerprint : null;
|
|
514
|
+
const updatedAt = typeof raw.updatedAt === "number" ? raw.updatedAt : 0;
|
|
515
|
+
if (!fingerprint || !title && !cwd) return null;
|
|
516
|
+
return { title, cwd, createdAt, fingerprint, updatedAt };
|
|
517
|
+
}
|
|
518
|
+
function normalizeTitleIndex(raw) {
|
|
519
|
+
const entries = {};
|
|
520
|
+
if (raw && typeof raw === "object" && raw.entries && typeof raw.entries === "object") {
|
|
521
|
+
for (const [id, entry] of Object.entries(raw.entries)) {
|
|
522
|
+
if (typeof id !== "string" || !id || id.length > 200) continue;
|
|
523
|
+
const normalized = normalizeEntry(entry);
|
|
524
|
+
if (normalized) entries[id] = normalized;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return { schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries };
|
|
528
|
+
}
|
|
529
|
+
function mergeEntries(left, right) {
|
|
530
|
+
const merged = { ...left };
|
|
531
|
+
for (const [id, entry] of Object.entries(right)) merged[id] = entry;
|
|
532
|
+
const ids = Object.keys(merged);
|
|
533
|
+
if (ids.length > MAX_ENTRIES) {
|
|
534
|
+
ids.sort((a, b) => (merged[a].updatedAt || 0) - (merged[b].updatedAt || 0));
|
|
535
|
+
for (const id of ids.slice(0, ids.length - MAX_ENTRIES)) delete merged[id];
|
|
536
|
+
}
|
|
537
|
+
return merged;
|
|
538
|
+
}
|
|
539
|
+
function createTitleIndexStore({ dir, file }) {
|
|
540
|
+
let cache = null;
|
|
541
|
+
let chain = Promise.resolve();
|
|
542
|
+
const path = file || join3(dir, "title-index.json");
|
|
543
|
+
async function readRaw() {
|
|
544
|
+
try {
|
|
545
|
+
return normalizeTitleIndex(JSON.parse(await readFile(path, "utf8")));
|
|
546
|
+
} catch (e) {
|
|
547
|
+
return normalizeTitleIndex(null);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function enqueue(mutator) {
|
|
551
|
+
const operation = chain.then(async () => {
|
|
552
|
+
const store = cache || (cache = (await readRaw()).entries);
|
|
553
|
+
await mutator(store);
|
|
554
|
+
return store;
|
|
555
|
+
});
|
|
556
|
+
chain = operation.catch(() => {
|
|
557
|
+
});
|
|
558
|
+
return operation;
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
// 只读:内存优先,未加载过才落盘一次。绝不抛错。
|
|
562
|
+
async entries() {
|
|
563
|
+
if (cache) return cache;
|
|
564
|
+
cache = (await readRaw()).entries;
|
|
565
|
+
return cache;
|
|
566
|
+
},
|
|
567
|
+
// 批量合并写入(原子替换)。失败静默:索引只是加速器,坏了下次重解码。
|
|
568
|
+
async merge(batch) {
|
|
569
|
+
const right = {};
|
|
570
|
+
for (const [id, entry] of Object.entries(batch || {})) {
|
|
571
|
+
const normalized = normalizeEntry(entry);
|
|
572
|
+
if (normalized) right[String(id)] = normalized;
|
|
573
|
+
}
|
|
574
|
+
if (!Object.keys(right).length) return false;
|
|
575
|
+
await enqueue(async (store) => {
|
|
576
|
+
const next = mergeEntries(store, right);
|
|
577
|
+
await mkdir3(dirname(path), { recursive: true });
|
|
578
|
+
const tmp = join3(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
|
|
579
|
+
await writeFile3(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: next }), { encoding: "utf8", mode: 384 });
|
|
580
|
+
await rename3(tmp, path);
|
|
581
|
+
cache = next;
|
|
582
|
+
});
|
|
583
|
+
return true;
|
|
584
|
+
},
|
|
585
|
+
async remove(ids) {
|
|
586
|
+
const wanted = new Set((ids || []).map(String));
|
|
587
|
+
if (!wanted.size) return false;
|
|
588
|
+
await enqueue(async (store) => {
|
|
589
|
+
let changed = false;
|
|
590
|
+
for (const id of wanted) {
|
|
591
|
+
if (id in store) {
|
|
592
|
+
delete store[id];
|
|
593
|
+
changed = true;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (!changed) return;
|
|
597
|
+
await mkdir3(dirname(path), { recursive: true });
|
|
598
|
+
const tmp = join3(dirname(path), `.title-index-${process.pid}-${Date.now()}.tmp`);
|
|
599
|
+
await writeFile3(tmp, JSON.stringify({ schemaVersion: TITLE_INDEX_SCHEMA_VERSION, entries: store }), { encoding: "utf8", mode: 384 });
|
|
600
|
+
await rename3(tmp, path);
|
|
601
|
+
});
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
503
607
|
// src/index.js
|
|
504
608
|
var name = "dsh-sessions-manager";
|
|
505
609
|
var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
|
|
506
610
|
var MAX_TITLE = 80;
|
|
507
|
-
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR ||
|
|
508
|
-
var TRASH_INDEX =
|
|
611
|
+
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR || join4(homedir3(), ".dsh", "sessions-manager-trash");
|
|
612
|
+
var TRASH_INDEX = join4(TRASH_DIR, "index.json");
|
|
509
613
|
var TRASH_SCHEMA_VERSION = 2;
|
|
510
614
|
var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
|
|
511
615
|
var FETCH_TOOL_RE = /search|fetch|download|browse/i;
|
|
@@ -590,6 +694,40 @@ function apply(ctx) {
|
|
|
590
694
|
const dom = () => ctx.storageDomain.get("workspace");
|
|
591
695
|
const authorityTitleCache = /* @__PURE__ */ new Map();
|
|
592
696
|
const metaCache = createSessionMetaCache();
|
|
697
|
+
const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join4(TRASH_DIR, "title-index.json") });
|
|
698
|
+
async function hydrateFromPersist(ids, statsById) {
|
|
699
|
+
const hits = /* @__PURE__ */ new Map();
|
|
700
|
+
if (!ids || !ids.length) return hits;
|
|
701
|
+
let store;
|
|
702
|
+
try {
|
|
703
|
+
store = await titleIndex.entries();
|
|
704
|
+
} catch (e) {
|
|
705
|
+
return hits;
|
|
706
|
+
}
|
|
707
|
+
for (const id of ids) {
|
|
708
|
+
const stat2 = statsById.get(id);
|
|
709
|
+
const entry = store && store[id];
|
|
710
|
+
if (!stat2 || !entry) continue;
|
|
711
|
+
const fp = fingerprintOf(stat2);
|
|
712
|
+
if (fp && entry.fingerprint === fp) {
|
|
713
|
+
hits.set(id, { title: entry.title, cwd: entry.cwd, createdAt: entry.createdAt });
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return hits;
|
|
717
|
+
}
|
|
718
|
+
function persistDecoded(decoded, statsById) {
|
|
719
|
+
if (!decoded || !decoded.size) return;
|
|
720
|
+
const batch = {};
|
|
721
|
+
const now = Date.now();
|
|
722
|
+
for (const [id, meta] of decoded) {
|
|
723
|
+
const fp = fingerprintOf(statsById.get(id));
|
|
724
|
+
if (!fp) continue;
|
|
725
|
+
batch[id] = { title: meta.title, cwd: meta.cwd, createdAt: meta.createdAt, fingerprint: fp, updatedAt: now };
|
|
726
|
+
}
|
|
727
|
+
if (!Object.keys(batch).length) return;
|
|
728
|
+
titleIndex.merge(batch).catch(() => {
|
|
729
|
+
});
|
|
730
|
+
}
|
|
593
731
|
function metaFromSnapshot(o) {
|
|
594
732
|
let title = null, createdAt = null, cwd = null;
|
|
595
733
|
if (o) {
|
|
@@ -686,6 +824,7 @@ function apply(ctx) {
|
|
|
686
824
|
}
|
|
687
825
|
}
|
|
688
826
|
metaCache.set(key, statInfo, meta);
|
|
827
|
+
if (opts.collectDecoded && statInfo) opts.collectDecoded(key, meta);
|
|
689
828
|
return buildItem(key, meta, usage, opts.exposeUsage);
|
|
690
829
|
}
|
|
691
830
|
async function collectUsage(preloadedHeaders) {
|
|
@@ -746,10 +885,10 @@ function apply(ctx) {
|
|
|
746
885
|
return (await readTrashStore()).items;
|
|
747
886
|
}
|
|
748
887
|
async function writeTrashStore(store) {
|
|
749
|
-
await
|
|
750
|
-
const tmp =
|
|
751
|
-
await
|
|
752
|
-
await
|
|
888
|
+
await mkdir4(TRASH_DIR, { recursive: true });
|
|
889
|
+
const tmp = join4(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
|
|
890
|
+
await writeFile4(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
|
|
891
|
+
await rename4(tmp, TRASH_INDEX);
|
|
753
892
|
}
|
|
754
893
|
function mutateTrash(mutator) {
|
|
755
894
|
const operation = trashMutation.then(async () => {
|
|
@@ -855,7 +994,7 @@ function apply(ctx) {
|
|
|
855
994
|
} catch (e) {
|
|
856
995
|
}
|
|
857
996
|
if (!target && typeof entry.originalPath === "string") target = entry.originalPath;
|
|
858
|
-
const targetOwnsSession = target && (basename(
|
|
997
|
+
const targetOwnsSession = target && (basename(dirname2(target)) === sid || basename(target).includes(sid));
|
|
859
998
|
if (target && !targetOwnsSession) {
|
|
860
999
|
const error = new Error("\u65E5\u5FD7\u8DEF\u5F84\u4E0E\u4F1A\u8BDD ID \u4E0D\u5339\u914D\uFF0C\u5DF2\u505C\u6B62\u6C38\u4E45\u5220\u9664");
|
|
861
1000
|
error.status = 409;
|
|
@@ -948,8 +1087,8 @@ function apply(ctx) {
|
|
|
948
1087
|
async function moveTargetWorkspace(rawPath) {
|
|
949
1088
|
if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
|
|
950
1089
|
let p = String(rawPath).trim();
|
|
951
|
-
if (p.startsWith("~/")) p =
|
|
952
|
-
if (!isAbsolute(p)) p =
|
|
1090
|
+
if (p.startsWith("~/")) p = join4(homedir3(), p.slice(2));
|
|
1091
|
+
if (!isAbsolute(p)) p = join4(homedir3(), p);
|
|
953
1092
|
let canonical = null;
|
|
954
1093
|
try {
|
|
955
1094
|
canonical = await realpath(p);
|
|
@@ -957,7 +1096,7 @@ function apply(ctx) {
|
|
|
957
1096
|
canonical = null;
|
|
958
1097
|
}
|
|
959
1098
|
if (canonical === null) {
|
|
960
|
-
await
|
|
1099
|
+
await mkdir4(p, { recursive: true });
|
|
961
1100
|
canonical = await realpath(p);
|
|
962
1101
|
}
|
|
963
1102
|
return { canonical, entity: await w.create(canonical, basename(canonical) || "workspace") };
|
|
@@ -995,13 +1134,13 @@ function apply(ctx) {
|
|
|
995
1134
|
if (!oldPath || !newPath || oldPath === newPath) return false;
|
|
996
1135
|
const backupPath = `${oldPath}.move-backup-${Date.now()}`;
|
|
997
1136
|
try {
|
|
998
|
-
await
|
|
999
|
-
await
|
|
1137
|
+
await mkdir4(dirname2(newPath), { recursive: true });
|
|
1138
|
+
await rename4(oldPath, backupPath);
|
|
1000
1139
|
await rewriteFrame0Cwd(backupPath, canonical);
|
|
1001
|
-
await
|
|
1140
|
+
await rename4(backupPath, newPath);
|
|
1002
1141
|
} catch (e) {
|
|
1003
1142
|
try {
|
|
1004
|
-
await
|
|
1143
|
+
await rename4(backupPath, oldPath);
|
|
1005
1144
|
} catch (_) {
|
|
1006
1145
|
}
|
|
1007
1146
|
if (e && e.code !== "ENOENT") throw e;
|
|
@@ -1047,7 +1186,7 @@ function apply(ctx) {
|
|
|
1047
1186
|
const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
|
|
1048
1187
|
if (backupPath) {
|
|
1049
1188
|
try {
|
|
1050
|
-
await
|
|
1189
|
+
await rename4(oldPath, backupPath);
|
|
1051
1190
|
} catch (e) {
|
|
1052
1191
|
if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
|
|
1053
1192
|
}
|
|
@@ -1055,7 +1194,7 @@ function apply(ctx) {
|
|
|
1055
1194
|
const restore = async () => {
|
|
1056
1195
|
if (backupPath) {
|
|
1057
1196
|
try {
|
|
1058
|
-
await
|
|
1197
|
+
await rename4(backupPath, oldPath);
|
|
1059
1198
|
} catch (_) {
|
|
1060
1199
|
}
|
|
1061
1200
|
}
|
|
@@ -1203,15 +1342,24 @@ function apply(ctx) {
|
|
|
1203
1342
|
const usage = await collectUsage(headers);
|
|
1204
1343
|
const statsById = new Map(visibleIds.map((id) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
|
|
1205
1344
|
const { cached, missing } = metaCache.partition(visibleIds, statsById);
|
|
1206
|
-
const
|
|
1345
|
+
const persisted = await hydrateFromPersist(missing, statsById);
|
|
1346
|
+
for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
|
|
1347
|
+
const stillMissing = missing.filter((id) => !persisted.has(id));
|
|
1348
|
+
const snapshotById = await projectTitles(stillMissing);
|
|
1349
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
1350
|
+
const collectDecoded = (id, meta) => {
|
|
1351
|
+
decoded.set(id, meta);
|
|
1352
|
+
};
|
|
1207
1353
|
const CHUNK = 6;
|
|
1208
1354
|
for (let i = 0; i < visibleIds.length; i += CHUNK) {
|
|
1209
1355
|
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => resolveOne(id, usage, {
|
|
1210
1356
|
exposeUsage: !!(opts && opts.usage),
|
|
1211
|
-
preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0
|
|
1357
|
+
preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0,
|
|
1358
|
+
collectDecoded
|
|
1212
1359
|
})));
|
|
1213
1360
|
for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
|
|
1214
1361
|
}
|
|
1362
|
+
persistDecoded(decoded, statsById);
|
|
1215
1363
|
let starredSet = /* @__PURE__ */ new Set();
|
|
1216
1364
|
try {
|
|
1217
1365
|
starredSet = new Set((await stars.read()).starredSessionIds);
|
|
@@ -1278,17 +1426,26 @@ function apply(ctx) {
|
|
|
1278
1426
|
const usage = await collectUsage(headers);
|
|
1279
1427
|
const statsById = new Map(ids.map((id) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
|
|
1280
1428
|
const { cached, missing } = metaCache.partition(ids, statsById);
|
|
1281
|
-
const
|
|
1429
|
+
const persisted = await hydrateFromPersist(missing, statsById);
|
|
1430
|
+
for (const [id, meta] of persisted) metaCache.set(id, statsById.get(id), meta);
|
|
1431
|
+
const rest = missing.filter((id) => !persisted.has(id));
|
|
1432
|
+
const snapshotById = await projectTitles(rest);
|
|
1433
|
+
const decoded = /* @__PURE__ */ new Map();
|
|
1434
|
+
const collectDecoded = (id, meta) => {
|
|
1435
|
+
decoded.set(id, meta);
|
|
1436
|
+
};
|
|
1282
1437
|
for (const id of ids) {
|
|
1283
|
-
let meta = cached.get(id) || null;
|
|
1438
|
+
let meta = cached.get(id) || persisted.get(id) || null;
|
|
1284
1439
|
if (!meta) {
|
|
1285
1440
|
const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
|
|
1286
1441
|
const next = metaFromSnapshot(snapshot);
|
|
1287
1442
|
metaCache.set(id, statsById.get(id), next);
|
|
1443
|
+
if (statsById.get(id)) collectDecoded(id, next);
|
|
1288
1444
|
meta = next;
|
|
1289
1445
|
}
|
|
1290
1446
|
if (meta && meta.title) authorityTitleCache.set(id, String(meta.title));
|
|
1291
1447
|
}
|
|
1448
|
+
persistDecoded(decoded, statsById);
|
|
1292
1449
|
}
|
|
1293
1450
|
return {
|
|
1294
1451
|
titles: Object.fromEntries(authorityTitleCache),
|
|
@@ -1619,6 +1776,8 @@ function apply(ctx) {
|
|
|
1619
1776
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
1620
1777
|
const out = await purgeFromTrash(sid);
|
|
1621
1778
|
metaCache.invalidate(sid);
|
|
1779
|
+
titleIndex.remove([sid]).catch(() => {
|
|
1780
|
+
});
|
|
1622
1781
|
json(res, out);
|
|
1623
1782
|
} catch (e) {
|
|
1624
1783
|
json(res, { ok: false, error: String(e && e.message || e) }, 500);
|
|
@@ -1638,6 +1797,8 @@ function apply(ctx) {
|
|
|
1638
1797
|
try {
|
|
1639
1798
|
results.push({ sessionId: sid, ok: true, ...await purgeFromTrash(sid) });
|
|
1640
1799
|
metaCache.invalidate(sid);
|
|
1800
|
+
titleIndex.remove([sid]).catch(() => {
|
|
1801
|
+
});
|
|
1641
1802
|
} catch (e) {
|
|
1642
1803
|
results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
|
|
1643
1804
|
}
|
|
@@ -1819,8 +1980,16 @@ function apply(ctx) {
|
|
|
1819
1980
|
const patch = {};
|
|
1820
1981
|
if (body && Object.prototype.hasOwnProperty.call(body, "inactiveDays")) patch.inactiveDays = body.inactiveDays;
|
|
1821
1982
|
if (body && Object.prototype.hasOwnProperty.call(body, "skipStarred")) patch.skipStarred = body.skipStarred;
|
|
1822
|
-
const
|
|
1823
|
-
const
|
|
1983
|
+
const isPatch = Object.keys(patch).length > 0;
|
|
1984
|
+
const settings = isPatch ? await autoArchive.update(patch) : (await autoArchive.read()).settings;
|
|
1985
|
+
let sweep;
|
|
1986
|
+
if (isPatch) {
|
|
1987
|
+
sweep = await autoArchiveSweep();
|
|
1988
|
+
} else {
|
|
1989
|
+
void autoArchiveSweep().catch(() => {
|
|
1990
|
+
});
|
|
1991
|
+
sweep = { triggered: true };
|
|
1992
|
+
}
|
|
1824
1993
|
const store = await autoArchive.read();
|
|
1825
1994
|
json(res, {
|
|
1826
1995
|
ok: true,
|