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/client.js +31 -4
- package/lib/client.js.map +2 -2
- package/lib/index.js +219 -73
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/client/index.jsx +47 -4
- package/src/index.js +165 -68
- package/src/session-meta-cache.js +117 -0
package/lib/index.js
CHANGED
|
@@ -405,10 +405,99 @@ 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
|
+
};
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
643
|
+
function buildItem(key, meta, usage, exposeUsage) {
|
|
644
|
+
const cwd = meta.cwd || null;
|
|
561
645
|
const ws = cwd ? wsByPath[cwd] : void 0;
|
|
562
|
-
const
|
|
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:
|
|
649
|
+
sessionId: key,
|
|
566
650
|
title: display,
|
|
567
|
-
createdAt: createdAt || null,
|
|
568
|
-
workspacePath: cwd
|
|
651
|
+
createdAt: meta.createdAt || null,
|
|
652
|
+
workspacePath: cwd,
|
|
569
653
|
workspaceTitle: ws && ws.title ? ws.title : null,
|
|
570
|
-
workspaceGone:
|
|
654
|
+
workspaceGone: !!(cwd && !ws),
|
|
571
655
|
hasWorkspace: !!cwd
|
|
572
656
|
};
|
|
573
|
-
if (usage) {
|
|
574
|
-
if (usage.sizeById && usage.sizeById.has(
|
|
575
|
-
if (usage.mtimeById && usage.mtimeById.has(
|
|
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
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1165
|
+
return out;
|
|
1166
|
+
}
|
|
1167
|
+
async function allSessionItems(opts = {}) {
|
|
1168
|
+
let headers = [];
|
|
1169
|
+
let headersOk = false;
|
|
1048
1170
|
try {
|
|
1049
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|