dsh-sessions-manager 3.4.0 → 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/client.js +31 -4
- package/lib/client.js.map +2 -2
- package/lib/index.js +408 -93
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/client/index.jsx +47 -4
- package/src/index.js +240 -71
- package/src/session-meta-cache.js +117 -0
- 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
|
|
|
@@ -405,18 +405,211 @@ function createAutoArchiveStore(options = {}) {
|
|
|
405
405
|
return store;
|
|
406
406
|
});
|
|
407
407
|
}
|
|
408
|
-
function
|
|
408
|
+
function isFresh2(store, now = Date.now()) {
|
|
409
409
|
return Number.isFinite(store && store.lastRunAt) && now - store.lastRunAt < RUN_INTERVAL_MS;
|
|
410
410
|
}
|
|
411
|
-
return { read, write, mutate, update, recordRun, isFresh, indexPath, dir };
|
|
411
|
+
return { read, write, mutate, update, recordRun, isFresh: isFresh2, indexPath, dir };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/session-meta-cache.js
|
|
415
|
+
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
416
|
+
var DEFAULT_MAX = 4e3;
|
|
417
|
+
function fingerprintOf(stat2) {
|
|
418
|
+
if (!stat2 || typeof stat2 !== "object") return null;
|
|
419
|
+
const mtimeMs = stat2.mtimeMs;
|
|
420
|
+
const size = stat2.size;
|
|
421
|
+
if (typeof mtimeMs !== "number" || !Number.isFinite(mtimeMs) || mtimeMs <= 0) return null;
|
|
422
|
+
if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return null;
|
|
423
|
+
return `${Math.floor(mtimeMs)}:${size}`;
|
|
424
|
+
}
|
|
425
|
+
function isFresh(entry, stat2, now, ttlMs = DEFAULT_TTL_MS) {
|
|
426
|
+
if (!entry) return false;
|
|
427
|
+
const fp = fingerprintOf(stat2);
|
|
428
|
+
if (!fp) return false;
|
|
429
|
+
if (entry.fingerprint !== fp) return false;
|
|
430
|
+
if (typeof entry.at !== "number") return false;
|
|
431
|
+
return now - entry.at <= ttlMs;
|
|
432
|
+
}
|
|
433
|
+
function partitionByCache(ids, statsById, cache, now = Date.now(), ttlMs = DEFAULT_TTL_MS) {
|
|
434
|
+
const cached = /* @__PURE__ */ new Map();
|
|
435
|
+
const missing = [];
|
|
436
|
+
for (const id of ids) {
|
|
437
|
+
const entry = cache && cache.get(String(id));
|
|
438
|
+
const stat2 = statsById && statsById.get(String(id));
|
|
439
|
+
if (isFresh(entry, stat2, now, ttlMs) && entry && entry.meta) {
|
|
440
|
+
cached.set(String(id), entry.meta);
|
|
441
|
+
} else {
|
|
442
|
+
missing.push(String(id));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return { cached, missing };
|
|
446
|
+
}
|
|
447
|
+
function createSessionMetaCache(opts = {}) {
|
|
448
|
+
const ttlMs = Number.isFinite(opts.ttlMs) ? opts.ttlMs : DEFAULT_TTL_MS;
|
|
449
|
+
const max = Number.isInteger(opts.max) && opts.max > 0 ? opts.max : DEFAULT_MAX;
|
|
450
|
+
const map = /* @__PURE__ */ new Map();
|
|
451
|
+
let hits = 0;
|
|
452
|
+
let misses = 0;
|
|
453
|
+
return {
|
|
454
|
+
// 命中返回 meta,未命中/无法校验返回 null。
|
|
455
|
+
get(id, stat2) {
|
|
456
|
+
const key = String(id);
|
|
457
|
+
const entry = map.get(key);
|
|
458
|
+
if (isFresh(entry, stat2, Date.now(), ttlMs)) {
|
|
459
|
+
hits++;
|
|
460
|
+
map.delete(key);
|
|
461
|
+
map.set(key, entry);
|
|
462
|
+
return entry.meta;
|
|
463
|
+
}
|
|
464
|
+
misses++;
|
|
465
|
+
return null;
|
|
466
|
+
},
|
|
467
|
+
set(id, stat2, meta) {
|
|
468
|
+
if (!meta) return null;
|
|
469
|
+
const fp = fingerprintOf(stat2);
|
|
470
|
+
if (!fp) return null;
|
|
471
|
+
const key = String(id);
|
|
472
|
+
map.delete(key);
|
|
473
|
+
map.set(key, { fingerprint: fp, at: Date.now(), meta });
|
|
474
|
+
if (map.size > max) {
|
|
475
|
+
const oldest = map.keys().next().value;
|
|
476
|
+
if (oldest !== void 0) map.delete(oldest);
|
|
477
|
+
}
|
|
478
|
+
return meta;
|
|
479
|
+
},
|
|
480
|
+
// 批量判定:一次算出「命中缓存」与「需要解码」两组,供列表构建做批量投影。
|
|
481
|
+
partition(ids, statsById) {
|
|
482
|
+
return partitionByCache(ids, statsById, map, Date.now(), ttlMs);
|
|
483
|
+
},
|
|
484
|
+
invalidate(id) {
|
|
485
|
+
if (id == null) return false;
|
|
486
|
+
const key = String(id);
|
|
487
|
+
const had = map.has(key);
|
|
488
|
+
map.delete(key);
|
|
489
|
+
return had;
|
|
490
|
+
},
|
|
491
|
+
clear() {
|
|
492
|
+
map.clear();
|
|
493
|
+
},
|
|
494
|
+
get size() {
|
|
495
|
+
return map.size;
|
|
496
|
+
},
|
|
497
|
+
stats() {
|
|
498
|
+
return { size: map.size, hits, misses, ttlMs };
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
}
|
|
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
|
+
};
|
|
412
605
|
}
|
|
413
606
|
|
|
414
607
|
// src/index.js
|
|
415
608
|
var name = "dsh-sessions-manager";
|
|
416
609
|
var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
|
|
417
610
|
var MAX_TITLE = 80;
|
|
418
|
-
var TRASH_DIR = process.env.DSH_SESSIONS_MANAGER_TRASH_DIR ||
|
|
419
|
-
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");
|
|
420
613
|
var TRASH_SCHEMA_VERSION = 2;
|
|
421
614
|
var DEFAULT_TRASH_SETTINGS = Object.freeze({ retentionDays: 0 });
|
|
422
615
|
var FETCH_TOOL_RE = /search|fetch|download|browse/i;
|
|
@@ -500,7 +693,58 @@ function apply(ctx) {
|
|
|
500
693
|
const sq = ctx.sessionQuery;
|
|
501
694
|
const dom = () => ctx.storageDomain.get("workspace");
|
|
502
695
|
const authorityTitleCache = /* @__PURE__ */ new Map();
|
|
503
|
-
|
|
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
|
+
}
|
|
731
|
+
function metaFromSnapshot(o) {
|
|
732
|
+
let title = null, createdAt = null, cwd = null;
|
|
733
|
+
if (o) {
|
|
734
|
+
if (o.title && o.title.title) title = String(o.title.title);
|
|
735
|
+
if (o.session) {
|
|
736
|
+
cwd = o.session.cwd || null;
|
|
737
|
+
createdAt = o.session.createdAt || null;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return { title, cwd, createdAt };
|
|
741
|
+
}
|
|
742
|
+
function unwrapSnapshot(result) {
|
|
743
|
+
if (!result) return null;
|
|
744
|
+
if (result.status === "fulfilled") return result.value || null;
|
|
745
|
+
if (result.status === "rejected") return null;
|
|
746
|
+
return result;
|
|
747
|
+
}
|
|
504
748
|
async function archivedState() {
|
|
505
749
|
const d = dom();
|
|
506
750
|
if (!d) throw new Error("workspace domain is not open");
|
|
@@ -534,56 +778,66 @@ function apply(ctx) {
|
|
|
534
778
|
return operation;
|
|
535
779
|
}
|
|
536
780
|
let wsByPath = {};
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
try {
|
|
540
|
-
const o = await sq.readTitleSnapshot(id);
|
|
541
|
-
if (o) {
|
|
542
|
-
if (o.title && o.title.title) title = String(o.title.title);
|
|
543
|
-
if (o.session) {
|
|
544
|
-
cwd = o.session.cwd || null;
|
|
545
|
-
createdAt = o.session.createdAt || null;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
} catch (e) {
|
|
549
|
-
}
|
|
550
|
-
if (!title || !cwd) {
|
|
551
|
-
try {
|
|
552
|
-
const r = await sp.readFrom(id, 0);
|
|
553
|
-
if (r.meta) {
|
|
554
|
-
if (!cwd) cwd = r.meta.cwd || null;
|
|
555
|
-
if (!createdAt) createdAt = r.meta.createdAt || null;
|
|
556
|
-
}
|
|
557
|
-
if (!title && Array.isArray(r.events)) title = foldTitle(r.events);
|
|
558
|
-
} catch (e2) {
|
|
559
|
-
}
|
|
560
|
-
}
|
|
781
|
+
function buildItem(key, meta, usage, exposeUsage) {
|
|
782
|
+
const cwd = meta.cwd || null;
|
|
561
783
|
const ws = cwd ? wsByPath[cwd] : void 0;
|
|
562
|
-
const
|
|
784
|
+
const title = meta.title || null;
|
|
563
785
|
const display = title ? String(title).length > MAX_TITLE ? String(title).slice(0, MAX_TITLE) + "\u2026" : String(title) : null;
|
|
564
786
|
const base = {
|
|
565
|
-
sessionId:
|
|
787
|
+
sessionId: key,
|
|
566
788
|
title: display,
|
|
567
|
-
createdAt: createdAt || null,
|
|
568
|
-
workspacePath: cwd
|
|
789
|
+
createdAt: meta.createdAt || null,
|
|
790
|
+
workspacePath: cwd,
|
|
569
791
|
workspaceTitle: ws && ws.title ? ws.title : null,
|
|
570
|
-
workspaceGone:
|
|
792
|
+
workspaceGone: !!(cwd && !ws),
|
|
571
793
|
hasWorkspace: !!cwd
|
|
572
794
|
};
|
|
573
|
-
if (usage) {
|
|
574
|
-
if (usage.sizeById && usage.sizeById.has(
|
|
575
|
-
if (usage.mtimeById && usage.mtimeById.has(
|
|
795
|
+
if (exposeUsage && usage) {
|
|
796
|
+
if (usage.sizeById && usage.sizeById.has(key)) base.sizeBytes = usage.sizeById.get(key);
|
|
797
|
+
if (usage.mtimeById && usage.mtimeById.has(key)) base.updatedAt = usage.mtimeById.get(key);
|
|
576
798
|
}
|
|
577
799
|
return base;
|
|
578
800
|
}
|
|
579
|
-
async function
|
|
801
|
+
async function resolveOne(id, usage, opts = {}) {
|
|
802
|
+
const key = String(id);
|
|
803
|
+
const statInfo = usage ? { mtimeMs: usage.mtimeById.get(key), size: usage.sizeById.get(key) } : null;
|
|
804
|
+
const cached = metaCache.get(key, statInfo);
|
|
805
|
+
if (cached) return buildItem(key, cached, usage, opts.exposeUsage);
|
|
806
|
+
let meta = { title: null, cwd: null, createdAt: null };
|
|
807
|
+
if (opts.preloaded !== void 0) {
|
|
808
|
+
meta = metaFromSnapshot(unwrapSnapshot(opts.preloaded));
|
|
809
|
+
} else if (typeof sq.readTitleSnapshot === "function") {
|
|
810
|
+
try {
|
|
811
|
+
meta = metaFromSnapshot(await sq.readTitleSnapshot(id));
|
|
812
|
+
} catch (e) {
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (!meta.title || !meta.cwd) {
|
|
816
|
+
try {
|
|
817
|
+
const r = await sp.readFrom(id, 0);
|
|
818
|
+
if (r.meta) {
|
|
819
|
+
if (!meta.cwd) meta.cwd = r.meta.cwd || null;
|
|
820
|
+
if (!meta.createdAt) meta.createdAt = r.meta.createdAt || null;
|
|
821
|
+
}
|
|
822
|
+
if (!meta.title && Array.isArray(r.events)) meta.title = foldTitle(r.events);
|
|
823
|
+
} catch (e2) {
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
metaCache.set(key, statInfo, meta);
|
|
827
|
+
if (opts.collectDecoded && statInfo) opts.collectDecoded(key, meta);
|
|
828
|
+
return buildItem(key, meta, usage, opts.exposeUsage);
|
|
829
|
+
}
|
|
830
|
+
async function collectUsage(preloadedHeaders) {
|
|
580
831
|
const sizeById = /* @__PURE__ */ new Map();
|
|
581
832
|
const mtimeById = /* @__PURE__ */ new Map();
|
|
582
|
-
let headers =
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
833
|
+
let headers = null;
|
|
834
|
+
if (Array.isArray(preloadedHeaders)) headers = preloadedHeaders;
|
|
835
|
+
else {
|
|
836
|
+
try {
|
|
837
|
+
headers = await sp.list();
|
|
838
|
+
} catch (e) {
|
|
839
|
+
headers = [];
|
|
840
|
+
}
|
|
587
841
|
}
|
|
588
842
|
if (!Array.isArray(headers)) headers = [];
|
|
589
843
|
const CHUNK = 8;
|
|
@@ -631,10 +885,10 @@ function apply(ctx) {
|
|
|
631
885
|
return (await readTrashStore()).items;
|
|
632
886
|
}
|
|
633
887
|
async function writeTrashStore(store) {
|
|
634
|
-
await
|
|
635
|
-
const tmp =
|
|
636
|
-
await
|
|
637
|
-
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);
|
|
638
892
|
}
|
|
639
893
|
function mutateTrash(mutator) {
|
|
640
894
|
const operation = trashMutation.then(async () => {
|
|
@@ -740,7 +994,7 @@ function apply(ctx) {
|
|
|
740
994
|
} catch (e) {
|
|
741
995
|
}
|
|
742
996
|
if (!target && typeof entry.originalPath === "string") target = entry.originalPath;
|
|
743
|
-
const targetOwnsSession = target && (basename(
|
|
997
|
+
const targetOwnsSession = target && (basename(dirname2(target)) === sid || basename(target).includes(sid));
|
|
744
998
|
if (target && !targetOwnsSession) {
|
|
745
999
|
const error = new Error("\u65E5\u5FD7\u8DEF\u5F84\u4E0E\u4F1A\u8BDD ID \u4E0D\u5339\u914D\uFF0C\u5DF2\u505C\u6B62\u6C38\u4E45\u5220\u9664");
|
|
746
1000
|
error.status = 409;
|
|
@@ -833,8 +1087,8 @@ function apply(ctx) {
|
|
|
833
1087
|
async function moveTargetWorkspace(rawPath) {
|
|
834
1088
|
if (typeof rawPath !== "string" || !rawPath.trim()) throw new Error("\u7F3A\u5C11\u76EE\u6807\u5DE5\u4F5C\u533A\u8DEF\u5F84");
|
|
835
1089
|
let p = String(rawPath).trim();
|
|
836
|
-
if (p.startsWith("~/")) p =
|
|
837
|
-
if (!isAbsolute(p)) p =
|
|
1090
|
+
if (p.startsWith("~/")) p = join4(homedir3(), p.slice(2));
|
|
1091
|
+
if (!isAbsolute(p)) p = join4(homedir3(), p);
|
|
838
1092
|
let canonical = null;
|
|
839
1093
|
try {
|
|
840
1094
|
canonical = await realpath(p);
|
|
@@ -842,7 +1096,7 @@ function apply(ctx) {
|
|
|
842
1096
|
canonical = null;
|
|
843
1097
|
}
|
|
844
1098
|
if (canonical === null) {
|
|
845
|
-
await
|
|
1099
|
+
await mkdir4(p, { recursive: true });
|
|
846
1100
|
canonical = await realpath(p);
|
|
847
1101
|
}
|
|
848
1102
|
return { canonical, entity: await w.create(canonical, basename(canonical) || "workspace") };
|
|
@@ -880,13 +1134,13 @@ function apply(ctx) {
|
|
|
880
1134
|
if (!oldPath || !newPath || oldPath === newPath) return false;
|
|
881
1135
|
const backupPath = `${oldPath}.move-backup-${Date.now()}`;
|
|
882
1136
|
try {
|
|
883
|
-
await
|
|
884
|
-
await
|
|
1137
|
+
await mkdir4(dirname2(newPath), { recursive: true });
|
|
1138
|
+
await rename4(oldPath, backupPath);
|
|
885
1139
|
await rewriteFrame0Cwd(backupPath, canonical);
|
|
886
|
-
await
|
|
1140
|
+
await rename4(backupPath, newPath);
|
|
887
1141
|
} catch (e) {
|
|
888
1142
|
try {
|
|
889
|
-
await
|
|
1143
|
+
await rename4(backupPath, oldPath);
|
|
890
1144
|
} catch (_) {
|
|
891
1145
|
}
|
|
892
1146
|
if (e && e.code !== "ENOENT") throw e;
|
|
@@ -932,7 +1186,7 @@ function apply(ctx) {
|
|
|
932
1186
|
const backupPath = oldPath ? `${oldPath}.move-backup-${Date.now()}` : null;
|
|
933
1187
|
if (backupPath) {
|
|
934
1188
|
try {
|
|
935
|
-
await
|
|
1189
|
+
await rename4(oldPath, backupPath);
|
|
936
1190
|
} catch (e) {
|
|
937
1191
|
if (e && e.code !== "ENOENT") throw new Error("\u79FB\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u5907\u4EFD\u65E7\u7684\u4F1A\u8BDD\u65E5\u5FD7");
|
|
938
1192
|
}
|
|
@@ -940,7 +1194,7 @@ function apply(ctx) {
|
|
|
940
1194
|
const restore = async () => {
|
|
941
1195
|
if (backupPath) {
|
|
942
1196
|
try {
|
|
943
|
-
await
|
|
1197
|
+
await rename4(backupPath, oldPath);
|
|
944
1198
|
} catch (_) {
|
|
945
1199
|
}
|
|
946
1200
|
}
|
|
@@ -1034,25 +1288,38 @@ function apply(ctx) {
|
|
|
1034
1288
|
return { next: null, value: { ok: true, archived: true } };
|
|
1035
1289
|
});
|
|
1036
1290
|
}
|
|
1037
|
-
async function
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1291
|
+
async function projectTitles(ids) {
|
|
1292
|
+
const out = /* @__PURE__ */ new Map();
|
|
1293
|
+
if (!ids || !ids.length) return out;
|
|
1294
|
+
if (typeof sq.readTitleSnapshots !== "function") return out;
|
|
1041
1295
|
try {
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1296
|
+
const results = await sq.readTitleSnapshots(ids);
|
|
1297
|
+
if (!Array.isArray(results)) return out;
|
|
1298
|
+
results.forEach((result, index) => {
|
|
1299
|
+
const id = String(ids[index]);
|
|
1300
|
+
out.set(id, unwrapSnapshot(result));
|
|
1301
|
+
});
|
|
1045
1302
|
} catch (e) {
|
|
1046
1303
|
}
|
|
1047
|
-
|
|
1304
|
+
return out;
|
|
1305
|
+
}
|
|
1306
|
+
async function allSessionItems(opts = {}) {
|
|
1307
|
+
let headers = [];
|
|
1308
|
+
let headersOk = false;
|
|
1048
1309
|
try {
|
|
1049
|
-
|
|
1310
|
+
headers = await sp.list();
|
|
1311
|
+
headersOk = Array.isArray(headers);
|
|
1312
|
+
if (!headersOk) headers = [];
|
|
1050
1313
|
} catch (e) {
|
|
1314
|
+
headers = [];
|
|
1051
1315
|
}
|
|
1316
|
+
let live = ctx.get("sessions");
|
|
1317
|
+
const ids = headers.map((h) => String(h.id));
|
|
1052
1318
|
if (live) {
|
|
1053
1319
|
try {
|
|
1054
1320
|
live.list().forEach((s) => {
|
|
1055
|
-
|
|
1321
|
+
const sid = String(s.id);
|
|
1322
|
+
if (!ids.includes(sid)) ids.push(sid);
|
|
1056
1323
|
});
|
|
1057
1324
|
} catch (e) {
|
|
1058
1325
|
}
|
|
@@ -1072,12 +1339,27 @@ function apply(ctx) {
|
|
|
1072
1339
|
}
|
|
1073
1340
|
const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
|
|
1074
1341
|
const items = [];
|
|
1075
|
-
const usage =
|
|
1342
|
+
const usage = await collectUsage(headers);
|
|
1343
|
+
const statsById = new Map(visibleIds.map((id) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
|
|
1344
|
+
const { cached, missing } = metaCache.partition(visibleIds, statsById);
|
|
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
|
+
};
|
|
1076
1353
|
const CHUNK = 6;
|
|
1077
1354
|
for (let i = 0; i < visibleIds.length; i += CHUNK) {
|
|
1078
|
-
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => resolveOne(id, usage
|
|
1355
|
+
const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => resolveOne(id, usage, {
|
|
1356
|
+
exposeUsage: !!(opts && opts.usage),
|
|
1357
|
+
preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0,
|
|
1358
|
+
collectDecoded
|
|
1359
|
+
})));
|
|
1079
1360
|
for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
|
|
1080
1361
|
}
|
|
1362
|
+
persistDecoded(decoded, statsById);
|
|
1081
1363
|
let starredSet = /* @__PURE__ */ new Set();
|
|
1082
1364
|
try {
|
|
1083
1365
|
starredSet = new Set((await stars.read()).starredSessionIds);
|
|
@@ -1123,35 +1405,47 @@ function apply(ctx) {
|
|
|
1123
1405
|
}
|
|
1124
1406
|
async function sidebarAuthority() {
|
|
1125
1407
|
const ids = [];
|
|
1408
|
+
let headers = [];
|
|
1126
1409
|
try {
|
|
1127
|
-
|
|
1410
|
+
headers = await sp.list();
|
|
1128
1411
|
} catch (e) {
|
|
1412
|
+
headers = [];
|
|
1129
1413
|
}
|
|
1414
|
+
if (!Array.isArray(headers)) headers = [];
|
|
1415
|
+
for (const header of headers) ids.push(String(header.id));
|
|
1130
1416
|
const sessions = ctx.get("sessions");
|
|
1131
1417
|
try {
|
|
1132
1418
|
if (sessions) sessions.list().forEach((session) => {
|
|
1133
|
-
|
|
1419
|
+
const sid = String(session.id);
|
|
1420
|
+
if (!ids.includes(sid)) ids.push(sid);
|
|
1134
1421
|
});
|
|
1135
1422
|
} catch (e) {
|
|
1136
1423
|
}
|
|
1137
1424
|
const store = await readTrashStore();
|
|
1138
|
-
if (
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1425
|
+
if (ids.length) {
|
|
1426
|
+
const usage = await collectUsage(headers);
|
|
1427
|
+
const statsById = new Map(ids.map((id) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
|
|
1428
|
+
const { cached, missing } = metaCache.partition(ids, statsById);
|
|
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
|
+
};
|
|
1437
|
+
for (const id of ids) {
|
|
1438
|
+
let meta = cached.get(id) || persisted.get(id) || null;
|
|
1439
|
+
if (!meta) {
|
|
1440
|
+
const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
|
|
1441
|
+
const next = metaFromSnapshot(snapshot);
|
|
1442
|
+
metaCache.set(id, statsById.get(id), next);
|
|
1443
|
+
if (statsById.get(id)) collectDecoded(id, next);
|
|
1444
|
+
meta = next;
|
|
1152
1445
|
}
|
|
1446
|
+
if (meta && meta.title) authorityTitleCache.set(id, String(meta.title));
|
|
1153
1447
|
}
|
|
1154
|
-
|
|
1448
|
+
persistDecoded(decoded, statsById);
|
|
1155
1449
|
}
|
|
1156
1450
|
return {
|
|
1157
1451
|
titles: Object.fromEntries(authorityTitleCache),
|
|
@@ -1380,7 +1674,9 @@ function apply(ctx) {
|
|
|
1380
1674
|
const body = await readJsonBody(req);
|
|
1381
1675
|
const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
|
|
1382
1676
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
1383
|
-
|
|
1677
|
+
const out = await deleteOne(sid);
|
|
1678
|
+
metaCache.invalidate(sid);
|
|
1679
|
+
json(res, out);
|
|
1384
1680
|
} catch (e) {
|
|
1385
1681
|
json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
|
|
1386
1682
|
}
|
|
@@ -1398,6 +1694,7 @@ function apply(ctx) {
|
|
|
1398
1694
|
for (const sid of ids) {
|
|
1399
1695
|
try {
|
|
1400
1696
|
results.push({ sessionId: sid, ok: true, ...await deleteOne(sid) });
|
|
1697
|
+
metaCache.invalidate(sid);
|
|
1401
1698
|
} catch (e) {
|
|
1402
1699
|
results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
|
|
1403
1700
|
}
|
|
@@ -1461,7 +1758,9 @@ function apply(ctx) {
|
|
|
1461
1758
|
const body = await readJsonBody(req);
|
|
1462
1759
|
const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
|
|
1463
1760
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
1464
|
-
|
|
1761
|
+
const out = await restoreFromTrash(sid);
|
|
1762
|
+
metaCache.invalidate(sid);
|
|
1763
|
+
json(res, out);
|
|
1465
1764
|
} catch (e) {
|
|
1466
1765
|
json(res, { ok: false, error: String(e && e.message || e) }, 500);
|
|
1467
1766
|
}
|
|
@@ -1475,7 +1774,11 @@ function apply(ctx) {
|
|
|
1475
1774
|
const body = await readJsonBody(req);
|
|
1476
1775
|
const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
|
|
1477
1776
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
1478
|
-
|
|
1777
|
+
const out = await purgeFromTrash(sid);
|
|
1778
|
+
metaCache.invalidate(sid);
|
|
1779
|
+
titleIndex.remove([sid]).catch(() => {
|
|
1780
|
+
});
|
|
1781
|
+
json(res, out);
|
|
1479
1782
|
} catch (e) {
|
|
1480
1783
|
json(res, { ok: false, error: String(e && e.message || e) }, 500);
|
|
1481
1784
|
}
|
|
@@ -1493,6 +1796,9 @@ function apply(ctx) {
|
|
|
1493
1796
|
for (const sid of ids) {
|
|
1494
1797
|
try {
|
|
1495
1798
|
results.push({ sessionId: sid, ok: true, ...await purgeFromTrash(sid) });
|
|
1799
|
+
metaCache.invalidate(sid);
|
|
1800
|
+
titleIndex.remove([sid]).catch(() => {
|
|
1801
|
+
});
|
|
1496
1802
|
} catch (e) {
|
|
1497
1803
|
results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
|
|
1498
1804
|
}
|
|
@@ -1592,6 +1898,7 @@ function apply(ctx) {
|
|
|
1592
1898
|
if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
|
|
1593
1899
|
if (!target) return json(res, { ok: false, error: "missing targetPath" }, 400);
|
|
1594
1900
|
const moved = await moveOne(sid, target);
|
|
1901
|
+
metaCache.invalidate(sid);
|
|
1595
1902
|
try {
|
|
1596
1903
|
await reindexRegistry();
|
|
1597
1904
|
} catch (e) {
|
|
@@ -1673,8 +1980,16 @@ function apply(ctx) {
|
|
|
1673
1980
|
const patch = {};
|
|
1674
1981
|
if (body && Object.prototype.hasOwnProperty.call(body, "inactiveDays")) patch.inactiveDays = body.inactiveDays;
|
|
1675
1982
|
if (body && Object.prototype.hasOwnProperty.call(body, "skipStarred")) patch.skipStarred = body.skipStarred;
|
|
1676
|
-
const
|
|
1677
|
-
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
|
+
}
|
|
1678
1993
|
const store = await autoArchive.read();
|
|
1679
1994
|
json(res, {
|
|
1680
1995
|
ok: true,
|