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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.js
2
- import { mkdir as mkdir3, realpath, rename as rename3, stat, unlink, writeFile as writeFile3 } from "node:fs/promises";
3
- import { basename, dirname, isAbsolute, join as join3 } from "node:path";
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 isFresh(store, now = Date.now()) {
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 || join3(homedir3(), ".dsh", "sessions-manager-trash");
419
- var TRASH_INDEX = join3(TRASH_DIR, "index.json");
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
- let authorityTitlesLoaded = false;
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
- async function resolveOne(id, usage) {
538
- let title = null, createdAt = null, cwd = null;
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 workspaceGone = !!(cwd && !ws);
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: id,
787
+ sessionId: key,
566
788
  title: display,
567
- createdAt: createdAt || null,
568
- workspacePath: cwd || null,
789
+ createdAt: meta.createdAt || null,
790
+ workspacePath: cwd,
569
791
  workspaceTitle: ws && ws.title ? ws.title : null,
570
- workspaceGone: workspaceGone ? true : false,
792
+ workspaceGone: !!(cwd && !ws),
571
793
  hasWorkspace: !!cwd
572
794
  };
573
- if (usage) {
574
- if (usage.sizeById && usage.sizeById.has(id)) base.sizeBytes = usage.sizeById.get(id);
575
- if (usage.mtimeById && usage.mtimeById.has(id)) base.updatedAt = usage.mtimeById.get(id);
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 collectUsage() {
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
- try {
584
- headers = await sp.list();
585
- } catch (e) {
586
- headers = [];
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 mkdir3(TRASH_DIR, { recursive: true });
635
- const tmp = join3(TRASH_DIR, `.index-${process.pid}-${Date.now()}.tmp`);
636
- await writeFile3(tmp, JSON.stringify(normalizeTrashStore(store), null, 2), { encoding: "utf8", mode: 384 });
637
- await rename3(tmp, TRASH_INDEX);
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(dirname(target)) === sid || basename(target).includes(sid));
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 = join3(homedir3(), p.slice(2));
837
- if (!isAbsolute(p)) p = join3(homedir3(), 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 mkdir3(p, { recursive: true });
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 mkdir3(dirname(newPath), { recursive: true });
884
- await rename3(oldPath, backupPath);
1137
+ await mkdir4(dirname2(newPath), { recursive: true });
1138
+ await rename4(oldPath, backupPath);
885
1139
  await rewriteFrame0Cwd(backupPath, canonical);
886
- await rename3(backupPath, newPath);
1140
+ await rename4(backupPath, newPath);
887
1141
  } catch (e) {
888
1142
  try {
889
- await rename3(backupPath, oldPath);
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 rename3(oldPath, backupPath);
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 rename3(backupPath, oldPath);
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 allSessionItems(opts = {}) {
1038
- let materialized = /* @__PURE__ */ new Set();
1039
- let live = ctx.get("sessions");
1040
- let headersOk = false;
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 headers = await sp.list();
1043
- materialized = new Set(headers.map((h) => String(h.id)));
1044
- headersOk = true;
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
- const ids = [];
1304
+ return out;
1305
+ }
1306
+ async function allSessionItems(opts = {}) {
1307
+ let headers = [];
1308
+ let headersOk = false;
1048
1309
  try {
1049
- for (const header of await sp.list()) ids.push(String(header.id));
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
- if (!ids.includes(String(s.id))) ids.push(String(s.id));
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 = opts && opts.usage ? await collectUsage() : null;
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
- for (const header of await sp.list()) ids.push(String(header.id));
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
- if (!ids.includes(String(session.id))) ids.push(String(session.id));
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 (!authorityTitlesLoaded && ids.length && typeof sq.readTitleSnapshots === "function") {
1139
- const results = await sq.readTitleSnapshots(ids);
1140
- results.forEach((result, index) => {
1141
- if (result && result.status === "fulfilled" && result.value && result.value.title && typeof result.value.title.title === "string") {
1142
- authorityTitleCache.set(ids[index], result.value.title.title);
1143
- }
1144
- });
1145
- authorityTitlesLoaded = true;
1146
- } else if (!authorityTitlesLoaded) {
1147
- for (const sid of ids) {
1148
- try {
1149
- const snapshot = await sq.readTitleSnapshot(sid);
1150
- if (snapshot && snapshot.title && typeof snapshot.title.title === "string") authorityTitleCache.set(sid, snapshot.title.title);
1151
- } catch (e) {
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
- authorityTitlesLoaded = true;
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
- json(res, await deleteOne(sid));
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
- json(res, await restoreFromTrash(sid));
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
- json(res, await purgeFromTrash(sid));
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 settings = Object.keys(patch).length ? await autoArchive.update(patch) : (await autoArchive.read()).settings;
1677
- const sweep = await autoArchiveSweep();
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,