dsh-vscode-mode 0.18.0 → 0.19.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/src/treeIndex.ts CHANGED
@@ -21,9 +21,9 @@ import { DirListError, listDirCheap } from './treeRead.js'
21
21
  import { LEGACY_TREE_SIDECARS, treeCacheFile } from './paths.js'
22
22
 
23
23
  // --region 常量
24
- /** 命中新鲜度:超过则视为过期,按需重列。 */
24
+ /** 硬 TTL:距最近实列确认达到此时间即过期,命中不续期。 */
25
25
  export const TREE_TTL_MS = 60_000
26
- /** 内存目录上限:超出按 ts 逐出最旧(常驻目录命中保活,不会误逐)。 */
26
+ /** 内存目录上限:超出按独立运行时 LRU 逐出,不改变确认时间。 */
27
27
  export const MEM_MAX_DIRS = 2000
28
28
  /** 落盘目录上限(按 ts 裁旧)。 */
29
29
  export const PERSIST_MAX_DIRS = 400
@@ -65,6 +65,8 @@ interface IndexState {
65
65
  cwd: string
66
66
  data: TreeIndexData
67
67
  stale: Set<string>
68
+ /** 插入顺序即访问顺序,仅运行时使用,不写入 v1 快照。 */
69
+ lru: Map<string, true>
68
70
  dirty: boolean
69
71
  persistTimer: ReturnType<typeof setTimeout> | null
70
72
  healTimer: ReturnType<typeof setTimeout> | null
@@ -75,14 +77,6 @@ interface IndexState {
75
77
 
76
78
  /** cwd → 索引状态(随会话销毁清理)。 */
77
79
  const states = new Map<string, IndexState>()
78
- /** 单调时钟:连续 put 的 ts 严格递增(同毫秒不并列,逐出/裁剪顺序可预期)。 */
79
- let clock = 0
80
-
81
- /** 取下一时间戳(不小于真实时间且严格递增)。 */
82
- function nextTs(): number {
83
- clock = Math.max(Date.now(), clock + 1)
84
- return clock
85
- }
86
80
 
87
81
  // --region 纯函数(可单测)
88
82
  /** 条目原始值 → 合法条目(非法返回 null)。 */
@@ -147,8 +141,9 @@ export function indexParse(text: string | null): TreeIndexData | null {
147
141
  if (!e) continue
148
142
  entries.push({ name: e.name, path: rel ? rel + '/' + e.name : e.name, type: e.type })
149
143
  }
150
- if (!entries.length) continue
151
- dirs[rel] = { ts: typeof dir.ts === 'number' ? dir.ts : 0, full: dir.full === true, entries }
144
+ // 丢弃非法条目后不能再声称快照完整;空目录本身仍是有效快照。
145
+ const full = dir.full === true && entries.length === dir.entries.length
146
+ dirs[rel] = { ts: typeof dir.ts === 'number' ? dir.ts : 0, full, entries }
152
147
  }
153
148
  return { v: 1, root: typeof raw.root === 'string' ? raw.root : null, dirs }
154
149
  } catch (error) {
@@ -162,15 +157,16 @@ export function trimPersist(data: TreeIndexData, maxDirs = PERSIST_MAX_DIRS, max
162
157
  const sorted = Object.entries(data.dirs).sort((a, b) => b[1].ts - a[1].ts)
163
158
  for (const [rel, dir] of sorted) {
164
159
  if (Object.keys(dirs).length >= maxDirs) break
165
- const full = dir.entries.length <= maxEntries
166
- dirs[rel] = { ts: dir.ts, full, entries: full ? dir.entries : dir.entries.slice(0, maxEntries) }
160
+ const truncated = dir.entries.length > maxEntries
161
+ const full = dir.full && !truncated
162
+ dirs[rel] = { ts: dir.ts, full, entries: truncated ? dir.entries.slice(0, maxEntries) : dir.entries }
167
163
  }
168
164
  return { v: 1, root: data.root, dirs }
169
165
  }
170
166
  // --endregion
171
167
 
172
168
  // --region 状态机
173
- /** 取(或建)某 cwd 的索引状态,并惰性加载落盘快照(合并:文件 ts 更新才采纳)。 */
169
+ /** 取(或建)某 cwd 的索引状态并惰性加载快照;加载期间真实列取优先,不被旧快照覆盖。 */
174
170
  function ensureState(ctx: Ctx, cwd: string): IndexState {
175
171
  const existing = states.get(cwd)
176
172
  if (existing) return existing
@@ -179,6 +175,7 @@ function ensureState(ctx: Ctx, cwd: string): IndexState {
179
175
  cwd,
180
176
  data: { v: 1, root: null, dirs: {} },
181
177
  stale: new Set(),
178
+ lru: new Map(),
182
179
  dirty: false,
183
180
  persistTimer: null,
184
181
  healTimer: null,
@@ -209,9 +206,12 @@ async function loadIndex(state: IndexState): Promise<void> {
209
206
  try {
210
207
  const parsed = indexParse(await readFile(treeCacheFile(state.cwd), 'utf8'))
211
208
  if (!parsed) return
212
- for (const [rel, dir] of Object.entries(parsed.dirs)) {
213
- const cur = state.data.dirs[rel]
214
- if (!cur || dir.ts > cur.ts) state.data.dirs[rel] = dir
209
+ // 旧快照先进入 LRU;加载期间完成的真实列取优先,不被磁盘覆盖。
210
+ const snapshots = Object.entries(parsed.dirs).sort((a, b) => a[1].ts - b[1].ts)
211
+ for (const [rel, dir] of snapshots) {
212
+ if (state.data.dirs[rel]) continue
213
+ state.data.dirs[rel] = dir
214
+ touchDir(state, rel)
215
215
  }
216
216
  if (parsed.root && !state.data.root) state.data.root = parsed.root
217
217
  } catch (error) { /* 缺失/损坏/失败均按空索引 */ }
@@ -220,34 +220,57 @@ async function loadIndex(state: IndexState): Promise<void> {
220
220
  }
221
221
  }
222
222
 
223
- /** 命中:新鲜且未失效 → 条目(并刷新 ts 保活);否则 null。 */
223
+ /**
224
+ * @private
225
+ * @author ddj 2026年10月22号
226
+ * @description 更新运行时访问顺序,超限时同步清理目录及失效标记。
227
+ * @param state 工作区索引状态
228
+ * @param rel 本次访问的相对目录
229
+ */
230
+ function touchDir(state: IndexState, rel: string): void {
231
+ state.lru.delete(rel)
232
+ state.lru.set(rel, true)
233
+ if (state.lru.size <= MEM_MAX_DIRS) return
234
+
235
+ const oldest = state.lru.keys().next().value
236
+ if (oldest === undefined) return
237
+ state.lru.delete(oldest)
238
+ state.stale.delete(oldest)
239
+ delete state.data.dirs[oldest]
240
+ }
241
+
242
+ /**
243
+ * @public
244
+ * @author ddj 2026年10月22号
245
+ * @description 完整、新鲜且未失效才命中;访问仅更新 LRU,不续期硬 TTL。
246
+ * @param cwd 工作区根
247
+ * @param rel 相对目录
248
+ * @returns 完整条目,或未命中时的 null
249
+ */
224
250
  export function hitIndex(cwd: string, rel: string): TreeEntry[] | null {
225
251
  const state = states.get(cwd)
226
252
  if (!state) return null
227
253
  const dir = state.data.dirs[rel]
228
- if (!dir || state.stale.has(rel)) return null
229
- const now = Date.now()
230
- if (now - dir.ts > TREE_TTL_MS) return null
231
- dir.ts = now
254
+ if (!dir || !dir.full || state.stale.has(rel)) return null
255
+ if (Date.now() - dir.ts >= TREE_TTL_MS) return null
256
+ touchDir(state, rel)
232
257
  return dir.entries
233
258
  }
234
259
 
235
- /** 更新某目录条目:写内存 + 打脏 + 防抖落盘;stale 撤销。 */
260
+ /**
261
+ * @public
262
+ * @author ddj 2026年10月22号
263
+ * @description 保存完整实列结果与确认时间,更新 LRU、撤销 stale 并防抖落盘。
264
+ * @param ctx 工作区上下文
265
+ * @param cwd 工作区根
266
+ * @param rel 相对目录
267
+ * @param entries 完整实列条目
268
+ */
236
269
  export function putIndex(ctx: Ctx, cwd: string, rel: string, entries: TreeEntry[]): void {
237
270
  const state = ensureState(ctx, cwd)
238
- const dirs = state.data.dirs
239
- if (!dirs[rel] && Object.keys(dirs).length >= MEM_MAX_DIRS) {
240
- let oldest = rel
241
- let oldestTs = Infinity
242
- for (const [k, v] of Object.entries(dirs)) {
243
- if (v.ts < oldestTs) {
244
- oldestTs = v.ts
245
- oldest = k
246
- }
247
- }
248
- delete dirs[oldest]
249
- }
250
- dirs[rel] = { ts: nextTs(), full: entries.length <= PERSIST_DIR_ENTRIES, entries }
271
+ // 实列结果在内存中始终完整;条目上限仅约束持久化副本。
272
+ state.data.dirs[rel] = { ts: Date.now(), full: true, entries }
273
+ touchDir(state, rel)
251
274
  state.stale.delete(rel)
252
275
  state.dirty = true
253
276
  schedulePersist(state)
@@ -319,6 +342,7 @@ export function disposeIndex(cwd: string): void {
319
342
  state.healTimer = null
320
343
  }
321
344
  if (state.dirty) void persistNow(state)
345
+ state.lru.clear()
322
346
  states.delete(cwd)
323
347
  }
324
348
  // --endregion
@@ -379,6 +403,7 @@ export async function listDirCached(
379
403
  if (error instanceof DirListError && error.code === 'not-found') {
380
404
  if (state.data.dirs[rel]) {
381
405
  delete state.data.dirs[rel]
406
+ state.lru.delete(rel)
382
407
  state.dirty = true
383
408
  schedulePersist(state)
384
409
  }