dsh-sessions-manager 3.7.3 → 3.7.5

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
@@ -960,7 +960,9 @@ function pickInactiveCandidates(items, options = {}) {
960
960
  const now = Number.isFinite(options.now) ? options.now : Date.now();
961
961
  const cutoff = now - days * DAY_MS;
962
962
  const skipStarred = options.skipStarred !== false;
963
- const activeId = options.activeSessionId != null ? String(options.activeSessionId) : null;
963
+ const activeIds = new Set(
964
+ Array.isArray(options.activeSessionIds) ? options.activeSessionIds.map((id) => String(id)) : options.activeSessionId != null ? [String(options.activeSessionId)] : []
965
+ );
964
966
  const list = Array.isArray(items) ? items : [];
965
967
  const out = [];
966
968
  const seen = /* @__PURE__ */ new Set();
@@ -970,7 +972,7 @@ function pickInactiveCandidates(items, options = {}) {
970
972
  if (seen.has(id)) continue;
971
973
  if (item.archived) continue;
972
974
  if (skipStarred && item.starred) continue;
973
- if (activeId !== null && id === activeId) continue;
975
+ if (activeIds.has(id)) continue;
974
976
  const updatedAt = Number(item.updatedAt);
975
977
  if (!Number.isFinite(updatedAt) || updatedAt <= 0) continue;
976
978
  if (updatedAt < cutoff) {
@@ -1350,6 +1352,58 @@ function createEmptyScanStore({ dir, file }) {
1350
1352
  };
1351
1353
  }
1352
1354
 
1355
+ // src/active-session.js
1356
+ var DEFAULT_ACTIVE_TTL_MS = 45 * 1e3;
1357
+ var MAX_ACTIVE_REPORTS = 32;
1358
+ function isUsableSessionId(value) {
1359
+ return typeof value === "string" && value.length > 0 && value.length <= 200 && !/[\\/\0]/.test(value) && value !== "." && value !== "..";
1360
+ }
1361
+ function createActiveSessionTracker(options = {}) {
1362
+ const ttl = Number.isFinite(options.ttlMs) ? Number(options.ttlMs) : DEFAULT_ACTIVE_TTL_MS;
1363
+ const now = typeof options.now === "function" ? options.now : () => Date.now();
1364
+ const reports = /* @__PURE__ */ new Map();
1365
+ function trim(at) {
1366
+ for (const [key, value] of reports) if (at - value.at > ttl) reports.delete(key);
1367
+ while (reports.size > MAX_ACTIVE_REPORTS) {
1368
+ const oldest = reports.keys().next().value;
1369
+ if (oldest === void 0) break;
1370
+ reports.delete(oldest);
1371
+ }
1372
+ }
1373
+ function report(clientId, sessionId, at = now()) {
1374
+ const key = typeof clientId === "string" && clientId ? clientId.slice(0, 64) : "default";
1375
+ if (sessionId === null || sessionId === void 0 || sessionId === "") {
1376
+ reports.delete(key);
1377
+ return true;
1378
+ }
1379
+ if (!isUsableSessionId(sessionId)) return false;
1380
+ reports.set(key, { id: String(sessionId), at });
1381
+ trim(at);
1382
+ return true;
1383
+ }
1384
+ function activeIds(at = now()) {
1385
+ const out = /* @__PURE__ */ new Set();
1386
+ for (const [key, value] of reports) {
1387
+ if (at - value.at > ttl) {
1388
+ reports.delete(key);
1389
+ continue;
1390
+ }
1391
+ out.add(value.id);
1392
+ }
1393
+ return out;
1394
+ }
1395
+ return {
1396
+ report,
1397
+ activeIds,
1398
+ get size() {
1399
+ return reports.size;
1400
+ },
1401
+ clear() {
1402
+ reports.clear();
1403
+ }
1404
+ };
1405
+ }
1406
+
1353
1407
  // src/handle-era-paths.js
1354
1408
  import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
1355
1409
  import { basename, join as join10 } from "node:path";
@@ -1985,7 +2039,7 @@ function emptyScanCandidate(sizeBytes) {
1985
2039
  }
1986
2040
 
1987
2041
  // src/index.js
1988
- var BUILD_STAMP = true ? "3.7.3+5958fd65" : "dev";
2042
+ var BUILD_STAMP = true ? "3.7.5+23dc7e59" : "dev";
1989
2043
  var name = "dsh-sessions-manager";
1990
2044
  var inject = ["webServer", "workspaceRegistry", "sessionPersistence", "sessionQuery", "storageDomain"];
1991
2045
  var MAX_TITLE = 80;
@@ -2052,7 +2106,7 @@ function requireSessionId(value) {
2052
2106
  }
2053
2107
  return value;
2054
2108
  }
2055
- function getActiveSessionId(context) {
2109
+ function legacyActiveSessionId(context) {
2056
2110
  try {
2057
2111
  const a = context.get("activeSession");
2058
2112
  if (a != null) return a && a.id != null ? a.id : typeof a === "string" ? a : null;
@@ -2070,6 +2124,12 @@ function getActiveSessionId(context) {
2070
2124
  }
2071
2125
  return null;
2072
2126
  }
2127
+ function getActiveSessionIds(context, tracker) {
2128
+ const reported = tracker ? tracker.activeIds() : null;
2129
+ if (reported && reported.size) return reported;
2130
+ const legacy = legacyActiveSessionId(context);
2131
+ return legacy != null ? /* @__PURE__ */ new Set([String(legacy)]) : /* @__PURE__ */ new Set();
2132
+ }
2073
2133
  function apply(ctx) {
2074
2134
  const w = ctx.workspaceRegistry;
2075
2135
  const sp = ctx.sessionPersistence;
@@ -2081,6 +2141,7 @@ function apply(ctx) {
2081
2141
  const metaCache = createSessionMetaCache();
2082
2142
  const titleIndex = createTitleIndexStore({ dir: TRASH_DIR, file: join12(TRASH_DIR, "title-index.json") });
2083
2143
  const emptyIndex = createEmptyScanStore({ dir: TRASH_DIR });
2144
+ const activeTracker = createActiveSessionTracker();
2084
2145
  const EMPTY_VERDICT_TTL_MS = 14 * 24 * 3600 * 1e3;
2085
2146
  async function hydrateFromPersist(ids, statsById) {
2086
2147
  const hits = /* @__PURE__ */ new Map();
@@ -2137,6 +2198,19 @@ function apply(ctx) {
2137
2198
  if (result.status === "rejected") return null;
2138
2199
  return result;
2139
2200
  }
2201
+ function projectionTitleOf(liveStore, id) {
2202
+ try {
2203
+ const reg = ctx.get("sessionProjections");
2204
+ if (!reg || typeof reg.cachedSnapshot !== "function") return null;
2205
+ const session = liveStore && liveStore.get && liveStore.get(id);
2206
+ if (!session) return null;
2207
+ const snap = reg.cachedSnapshot(session, ["title"]);
2208
+ const title = snap && snap.values ? snap.values.title : null;
2209
+ return typeof title === "string" && title.length > 0 ? title : null;
2210
+ } catch (e) {
2211
+ return null;
2212
+ }
2213
+ }
2140
2214
  async function archivedState() {
2141
2215
  const d = dom();
2142
2216
  if (!d) throw new Error("workspace domain is not open");
@@ -2756,8 +2830,9 @@ function apply(ctx) {
2756
2830
  }
2757
2831
  async function moveOne(sid, targetPath) {
2758
2832
  requireCapability(capabilities, "move");
2759
- const activeId = getActiveSessionId(ctx);
2760
- if (activeId != null && String(activeId) === String(sid)) {
2833
+ const activeIds = getActiveSessionIds(ctx, activeTracker);
2834
+ const activeId = activeIds.values().next().value != null ? activeIds.values().next().value : null;
2835
+ if (activeIds.has(String(sid)) || activeId != null && String(activeId) === String(sid)) {
2761
2836
  const error = new Error("\u4F1A\u8BDD\u6B63\u88AB DSH \u6253\u5F00\uFF0C\u6682\u65F6\u65E0\u6CD5\u79FB\u52A8\uFF1B\u8BF7\u91CD\u542F DSH \u540E\u518D\u8BD5\u3002");
2762
2837
  error.status = 409;
2763
2838
  error.code = "DSM_SESSION_BUSY";
@@ -3225,11 +3300,29 @@ function apply(ctx) {
3225
3300
  createdAt: header && header.createdAt != null ? header.createdAt : meta.createdAt
3226
3301
  });
3227
3302
  }
3228
- const stillMissing = missing.filter((id) => !persisted.has(id));
3303
+ const stillMissing0 = missing.filter((id) => !persisted.has(id));
3304
+ const projectionMeta = /* @__PURE__ */ new Map();
3305
+ const stillMissing = [];
3306
+ for (const id of stillMissing0) {
3307
+ const title = projectionTitleOf(live, id);
3308
+ if (title == null) {
3309
+ stillMissing.push(id);
3310
+ continue;
3311
+ }
3312
+ const entry = entryById.get(id);
3313
+ const header = entry && entry.header ? entry.header : null;
3314
+ const meta = {
3315
+ title,
3316
+ cwd: header && typeof header.cwd === "string" && header.cwd ? header.cwd : null,
3317
+ createdAt: header && header.createdAt != null ? header.createdAt : null
3318
+ };
3319
+ projectionMeta.set(id, meta);
3320
+ metaCache.set(id, statsById.get(id), meta);
3321
+ }
3229
3322
  if (stillMissing.length) enqueueWarm(stillMissing, statsById, entryById);
3230
3323
  const items = [];
3231
3324
  for (const id of visibleIds) {
3232
- let meta = metaCache.get(id, statsById.get(id)) || persisted.get(id) || null;
3325
+ let meta = metaCache.get(id, statsById.get(id)) || persisted.get(id) || projectionMeta.get(id) || null;
3233
3326
  if (!meta) {
3234
3327
  meta = { title: null, cwd: null, createdAt: null };
3235
3328
  const entry = entryById.get(id);
@@ -3300,7 +3393,7 @@ function apply(ctx) {
3300
3393
  const candidates = pickInactiveCandidates(items, {
3301
3394
  inactiveDays: days,
3302
3395
  skipStarred: store.settings.skipStarred,
3303
- activeSessionId: getActiveSessionId(ctx),
3396
+ activeSessionIds: [...getActiveSessionIds(ctx, activeTracker)],
3304
3397
  now
3305
3398
  });
3306
3399
  let archived = 0;
@@ -3636,6 +3729,20 @@ function apply(ctx) {
3636
3729
  path: "/archived-sessions/capabilities",
3637
3730
  handler: async (req, res) => json(res, { ...capabilities, buildStamp: BUILD_STAMP })
3638
3731
  }));
3732
+ disposers.push(ctx.webServer.register({
3733
+ kind: "exact",
3734
+ path: "/archived-sessions/active-session",
3735
+ handler: async (req, res) => {
3736
+ const body = await readJsonBody(req);
3737
+ const clientId = body && typeof body.clientId === "string" ? body.clientId : "default";
3738
+ const sid = body && typeof body.sessionId === "string" && body.sessionId ? body.sessionId : null;
3739
+ const accepted = activeTracker.report(clientId, sid);
3740
+ json(res, {
3741
+ ok: accepted,
3742
+ activeSessionIds: [...activeTracker.activeIds()]
3743
+ });
3744
+ }
3745
+ }));
3639
3746
  disposers.push(ctx.webServer.register({
3640
3747
  kind: "exact",
3641
3748
  path: "/archived-sessions/list",