| 313 | async listFiles(): Promise<string[]> { |
| 314 | const results: string[] = []; |
| 315 | const walk = async (dirRel: string): Promise<void> => { |
| 316 | // Bounded walk: files may have been edited outside MemoryService. +1 lets |
| 317 | // callers detect overflow (e.g. the index logs its truncation). |
| 318 | if (results.length > MEMORY_MAX_FILES_PER_SCOPE) return; |
| 319 | let entries; |
| 320 | try { |
| 321 | entries = await fsPromises.readdir(this.abs(dirRel), { withFileTypes: true }); |
| 322 | } catch { |
| 323 | return; // Self-healing: missing/unreadable dirs list as empty. |
| 324 | } |
| 325 | // Iterate in path-string order — directories key as "name/" so the DFS |
| 326 | // emits exact global lexicographic order ("a.md" < "a/...", `.` < `/`). |
| 327 | // The capped subset is deterministic across platforms. |
| 328 | const sortKey = (entry: (typeof entries)[number]) => |
| 329 | entry.isDirectory() ? `${entry.name}/` : entry.name; |
| 330 | entries.sort((a, b) => { |
| 331 | const ka = sortKey(a); |
| 332 | const kb = sortKey(b); |
| 333 | return ka < kb ? -1 : ka > kb ? 1 : 0; |
| 334 | }); |
| 335 | for (const entry of entries) { |
| 336 | // Per-entry cap: a single flat directory can exceed the cap on its own. |
| 337 | if (results.length > MEMORY_MAX_FILES_PER_SCOPE) return; |
| 338 | if (entry.name.startsWith(".")) continue; |
| 339 | const childRel = dirRel === "" ? entry.name : `${dirRel}/${entry.name}`; |
| 340 | if (entry.isDirectory()) { |
| 341 | await walk(childRel); |
| 342 | } else if (entry.isFile()) { |
| 343 | results.push(childRel); |
| 344 | } |
| 345 | } |
| 346 | }; |
| 347 | await walk(""); |
| 348 | return results.sort(); |
| 349 | } |