dsh-sessions-manager 3.4.0 → 3.4.1

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
@@ -405,10 +405,99 @@ 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
+ };
412
501
  }
413
502
 
414
503
  // src/index.js
@@ -500,7 +589,24 @@ function apply(ctx) {
500
589
  const sq = ctx.sessionQuery;
501
590
  const dom = () => ctx.storageDomain.get("workspace");
502
591
  const authorityTitleCache = /* @__PURE__ */ new Map();
503
- let authorityTitlesLoaded = false;
592
+ const metaCache = createSessionMetaCache();
593
+ function metaFromSnapshot(o) {
594
+ let title = null, createdAt = null, cwd = null;
595
+ if (o) {
596
+ if (o.title && o.title.title) title = String(o.title.title);
597
+ if (o.session) {
598
+ cwd = o.session.cwd || null;
599
+ createdAt = o.session.createdAt || null;
600
+ }
601
+ }
602
+ return { title, cwd, createdAt };
603
+ }
604
+ function unwrapSnapshot(result) {
605
+ if (!result) return null;
606
+ if (result.status === "fulfilled") return result.value || null;
607
+ if (result.status === "rejected") return null;
608
+ return result;
609
+ }
504
610
  async function archivedState() {
505
611
  const d = dom();
506
612
  if (!d) throw new Error("workspace domain is not open");
@@ -534,56 +640,65 @@ function apply(ctx) {
534
640
  return operation;
535
641
  }
536
642
  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
- }
643
+ function buildItem(key, meta, usage, exposeUsage) {
644
+ const cwd = meta.cwd || null;
561
645
  const ws = cwd ? wsByPath[cwd] : void 0;
562
- const workspaceGone = !!(cwd && !ws);
646
+ const title = meta.title || null;
563
647
  const display = title ? String(title).length > MAX_TITLE ? String(title).slice(0, MAX_TITLE) + "\u2026" : String(title) : null;
564
648
  const base = {
565
- sessionId: id,
649
+ sessionId: key,
566
650
  title: display,
567
- createdAt: createdAt || null,
568
- workspacePath: cwd || null,
651
+ createdAt: meta.createdAt || null,
652
+ workspacePath: cwd,
569
653
  workspaceTitle: ws && ws.title ? ws.title : null,
570
- workspaceGone: workspaceGone ? true : false,
654
+ workspaceGone: !!(cwd && !ws),
571
655
  hasWorkspace: !!cwd
572
656
  };
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);
657
+ if (exposeUsage && usage) {
658
+ if (usage.sizeById && usage.sizeById.has(key)) base.sizeBytes = usage.sizeById.get(key);
659
+ if (usage.mtimeById && usage.mtimeById.has(key)) base.updatedAt = usage.mtimeById.get(key);
576
660
  }
577
661
  return base;
578
662
  }
579
- async function collectUsage() {
663
+ async function resolveOne(id, usage, opts = {}) {
664
+ const key = String(id);
665
+ const statInfo = usage ? { mtimeMs: usage.mtimeById.get(key), size: usage.sizeById.get(key) } : null;
666
+ const cached = metaCache.get(key, statInfo);
667
+ if (cached) return buildItem(key, cached, usage, opts.exposeUsage);
668
+ let meta = { title: null, cwd: null, createdAt: null };
669
+ if (opts.preloaded !== void 0) {
670
+ meta = metaFromSnapshot(unwrapSnapshot(opts.preloaded));
671
+ } else if (typeof sq.readTitleSnapshot === "function") {
672
+ try {
673
+ meta = metaFromSnapshot(await sq.readTitleSnapshot(id));
674
+ } catch (e) {
675
+ }
676
+ }
677
+ if (!meta.title || !meta.cwd) {
678
+ try {
679
+ const r = await sp.readFrom(id, 0);
680
+ if (r.meta) {
681
+ if (!meta.cwd) meta.cwd = r.meta.cwd || null;
682
+ if (!meta.createdAt) meta.createdAt = r.meta.createdAt || null;
683
+ }
684
+ if (!meta.title && Array.isArray(r.events)) meta.title = foldTitle(r.events);
685
+ } catch (e2) {
686
+ }
687
+ }
688
+ metaCache.set(key, statInfo, meta);
689
+ return buildItem(key, meta, usage, opts.exposeUsage);
690
+ }
691
+ async function collectUsage(preloadedHeaders) {
580
692
  const sizeById = /* @__PURE__ */ new Map();
581
693
  const mtimeById = /* @__PURE__ */ new Map();
582
- let headers = [];
583
- try {
584
- headers = await sp.list();
585
- } catch (e) {
586
- headers = [];
694
+ let headers = null;
695
+ if (Array.isArray(preloadedHeaders)) headers = preloadedHeaders;
696
+ else {
697
+ try {
698
+ headers = await sp.list();
699
+ } catch (e) {
700
+ headers = [];
701
+ }
587
702
  }
588
703
  if (!Array.isArray(headers)) headers = [];
589
704
  const CHUNK = 8;
@@ -1034,25 +1149,38 @@ function apply(ctx) {
1034
1149
  return { next: null, value: { ok: true, archived: true } };
1035
1150
  });
1036
1151
  }
1037
- async function allSessionItems(opts = {}) {
1038
- let materialized = /* @__PURE__ */ new Set();
1039
- let live = ctx.get("sessions");
1040
- let headersOk = false;
1152
+ async function projectTitles(ids) {
1153
+ const out = /* @__PURE__ */ new Map();
1154
+ if (!ids || !ids.length) return out;
1155
+ if (typeof sq.readTitleSnapshots !== "function") return out;
1041
1156
  try {
1042
- const headers = await sp.list();
1043
- materialized = new Set(headers.map((h) => String(h.id)));
1044
- headersOk = true;
1157
+ const results = await sq.readTitleSnapshots(ids);
1158
+ if (!Array.isArray(results)) return out;
1159
+ results.forEach((result, index) => {
1160
+ const id = String(ids[index]);
1161
+ out.set(id, unwrapSnapshot(result));
1162
+ });
1045
1163
  } catch (e) {
1046
1164
  }
1047
- const ids = [];
1165
+ return out;
1166
+ }
1167
+ async function allSessionItems(opts = {}) {
1168
+ let headers = [];
1169
+ let headersOk = false;
1048
1170
  try {
1049
- for (const header of await sp.list()) ids.push(String(header.id));
1171
+ headers = await sp.list();
1172
+ headersOk = Array.isArray(headers);
1173
+ if (!headersOk) headers = [];
1050
1174
  } catch (e) {
1175
+ headers = [];
1051
1176
  }
1177
+ let live = ctx.get("sessions");
1178
+ const ids = headers.map((h) => String(h.id));
1052
1179
  if (live) {
1053
1180
  try {
1054
1181
  live.list().forEach((s) => {
1055
- if (!ids.includes(String(s.id))) ids.push(String(s.id));
1182
+ const sid = String(s.id);
1183
+ if (!ids.includes(sid)) ids.push(sid);
1056
1184
  });
1057
1185
  } catch (e) {
1058
1186
  }
@@ -1072,10 +1200,16 @@ function apply(ctx) {
1072
1200
  }
1073
1201
  const currentArchived = new Set((await archivedState().catch(() => ({ archivedSessionIds: [] }))).archivedSessionIds || []);
1074
1202
  const items = [];
1075
- const usage = opts && opts.usage ? await collectUsage() : null;
1203
+ const usage = await collectUsage(headers);
1204
+ const statsById = new Map(visibleIds.map((id) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
1205
+ const { cached, missing } = metaCache.partition(visibleIds, statsById);
1206
+ const snapshotById = await projectTitles(missing);
1076
1207
  const CHUNK = 6;
1077
1208
  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)));
1209
+ const res2 = await Promise.all(visibleIds.slice(i, i + CHUNK).map((id) => resolveOne(id, usage, {
1210
+ exposeUsage: !!(opts && opts.usage),
1211
+ preloaded: snapshotById.has(id) ? snapshotById.get(id) : void 0
1212
+ })));
1079
1213
  for (const it of res2) items.push({ ...it, archived: currentArchived.has(it.sessionId) });
1080
1214
  }
1081
1215
  let starredSet = /* @__PURE__ */ new Set();
@@ -1123,35 +1257,38 @@ function apply(ctx) {
1123
1257
  }
1124
1258
  async function sidebarAuthority() {
1125
1259
  const ids = [];
1260
+ let headers = [];
1126
1261
  try {
1127
- for (const header of await sp.list()) ids.push(String(header.id));
1262
+ headers = await sp.list();
1128
1263
  } catch (e) {
1264
+ headers = [];
1129
1265
  }
1266
+ if (!Array.isArray(headers)) headers = [];
1267
+ for (const header of headers) ids.push(String(header.id));
1130
1268
  const sessions = ctx.get("sessions");
1131
1269
  try {
1132
1270
  if (sessions) sessions.list().forEach((session) => {
1133
- if (!ids.includes(String(session.id))) ids.push(String(session.id));
1271
+ const sid = String(session.id);
1272
+ if (!ids.includes(sid)) ids.push(sid);
1134
1273
  });
1135
1274
  } catch (e) {
1136
1275
  }
1137
1276
  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) {
1277
+ if (ids.length) {
1278
+ const usage = await collectUsage(headers);
1279
+ const statsById = new Map(ids.map((id) => [id, { mtimeMs: usage.mtimeById.get(id), size: usage.sizeById.get(id) }]));
1280
+ const { cached, missing } = metaCache.partition(ids, statsById);
1281
+ const snapshotById = await projectTitles(missing);
1282
+ for (const id of ids) {
1283
+ let meta = cached.get(id) || null;
1284
+ if (!meta) {
1285
+ const snapshot = snapshotById.has(id) ? snapshotById.get(id) : typeof sq.readTitleSnapshot === "function" ? await sq.readTitleSnapshot(id).catch(() => null) : null;
1286
+ const next = metaFromSnapshot(snapshot);
1287
+ metaCache.set(id, statsById.get(id), next);
1288
+ meta = next;
1152
1289
  }
1290
+ if (meta && meta.title) authorityTitleCache.set(id, String(meta.title));
1153
1291
  }
1154
- authorityTitlesLoaded = true;
1155
1292
  }
1156
1293
  return {
1157
1294
  titles: Object.fromEntries(authorityTitleCache),
@@ -1380,7 +1517,9 @@ function apply(ctx) {
1380
1517
  const body = await readJsonBody(req);
1381
1518
  const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
1382
1519
  if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
1383
- json(res, await deleteOne(sid));
1520
+ const out = await deleteOne(sid);
1521
+ metaCache.invalidate(sid);
1522
+ json(res, out);
1384
1523
  } catch (e) {
1385
1524
  json(res, { ok: false, error: String(e && e.message || e) }, errorStatus(e));
1386
1525
  }
@@ -1398,6 +1537,7 @@ function apply(ctx) {
1398
1537
  for (const sid of ids) {
1399
1538
  try {
1400
1539
  results.push({ sessionId: sid, ok: true, ...await deleteOne(sid) });
1540
+ metaCache.invalidate(sid);
1401
1541
  } catch (e) {
1402
1542
  results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
1403
1543
  }
@@ -1461,7 +1601,9 @@ function apply(ctx) {
1461
1601
  const body = await readJsonBody(req);
1462
1602
  const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
1463
1603
  if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
1464
- json(res, await restoreFromTrash(sid));
1604
+ const out = await restoreFromTrash(sid);
1605
+ metaCache.invalidate(sid);
1606
+ json(res, out);
1465
1607
  } catch (e) {
1466
1608
  json(res, { ok: false, error: String(e && e.message || e) }, 500);
1467
1609
  }
@@ -1475,7 +1617,9 @@ function apply(ctx) {
1475
1617
  const body = await readJsonBody(req);
1476
1618
  const sid = body && typeof body.sessionId === "string" ? body.sessionId : null;
1477
1619
  if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
1478
- json(res, await purgeFromTrash(sid));
1620
+ const out = await purgeFromTrash(sid);
1621
+ metaCache.invalidate(sid);
1622
+ json(res, out);
1479
1623
  } catch (e) {
1480
1624
  json(res, { ok: false, error: String(e && e.message || e) }, 500);
1481
1625
  }
@@ -1493,6 +1637,7 @@ function apply(ctx) {
1493
1637
  for (const sid of ids) {
1494
1638
  try {
1495
1639
  results.push({ sessionId: sid, ok: true, ...await purgeFromTrash(sid) });
1640
+ metaCache.invalidate(sid);
1496
1641
  } catch (e) {
1497
1642
  results.push({ sessionId: sid, ok: false, error: String(e && e.message || e) });
1498
1643
  }
@@ -1592,6 +1737,7 @@ function apply(ctx) {
1592
1737
  if (!sid) return json(res, { ok: false, error: "missing sessionId" }, 400);
1593
1738
  if (!target) return json(res, { ok: false, error: "missing targetPath" }, 400);
1594
1739
  const moved = await moveOne(sid, target);
1740
+ metaCache.invalidate(sid);
1595
1741
  try {
1596
1742
  await reindexRegistry();
1597
1743
  } catch (e) {