Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place

add subfs caching tests and typed mock builders

nekomimi.pet b379b966 f83c24d0

verified
Changed files
+506 -21
apps
hosting-service
+473 -8
apps/hosting-service/src/lib/utils.test.ts
··· 1 1 import { describe, test, expect } from 'bun:test' 2 - import { sanitizePath, extractBlobCid } from './utils' 2 + import { sanitizePath, extractBlobCid, extractSubfsUris, expandSubfsNodes } from './utils' 3 3 import { CID } from 'multiformats' 4 + import { BlobRef } from '@atproto/lexicon' 5 + import type { 6 + Record as WispFsRecord, 7 + Directory as FsDirectory, 8 + Entry as FsEntry, 9 + File as FsFile, 10 + Subfs as FsSubfs, 11 + } from '@wisp/lexicons/types/place/wisp/fs' 12 + import type { 13 + Record as SubfsRecord, 14 + Directory as SubfsDirectory, 15 + Entry as SubfsEntry, 16 + File as SubfsFile, 17 + Subfs as SubfsSubfs, 18 + } from '@wisp/lexicons/types/place/wisp/subfs' 19 + import type { $Typed } from '@wisp/lexicons/util' 4 20 5 21 describe('sanitizePath', () => { 6 22 test('allows normal file paths', () => { ··· 31 47 32 48 test('blocks directory traversal in middle of path', () => { 33 49 expect(sanitizePath('images/../../../etc/passwd')).toBe('images/etc/passwd') 34 - // Note: sanitizePath only filters out ".." segments, doesn't resolve paths 35 50 expect(sanitizePath('a/b/../c')).toBe('a/b/c') 36 51 expect(sanitizePath('a/../b/../c')).toBe('a/b/c') 37 52 }) ··· 50 65 }) 51 66 52 67 test('blocks null bytes', () => { 53 - // Null bytes cause the entire segment to be filtered out 54 68 expect(sanitizePath('index.html\0.txt')).toBe('') 55 69 expect(sanitizePath('test\0')).toBe('') 56 - // Null byte in middle segment 57 70 expect(sanitizePath('css/bad\0name/styles.css')).toBe('css/styles.css') 58 71 }) 59 72 ··· 89 102 90 103 describe('extractBlobCid', () => { 91 104 const TEST_CID = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y' 92 - 105 + 93 106 test('extracts CID from IPLD link', () => { 94 107 const blobRef = { $link: TEST_CID } 95 108 expect(extractBlobCid(blobRef)).toBe(TEST_CID) ··· 103 116 }) 104 117 105 118 test('extracts CID from typed BlobRef with IPLD link', () => { 106 - const blobRef = { 119 + const blobRef = { 107 120 ref: { $link: TEST_CID } 108 121 } 109 122 expect(extractBlobCid(blobRef)).toBe(TEST_CID) ··· 129 142 }) 130 143 131 144 test('handles nested structures from AT Proto API', () => { 132 - // Real structure from AT Proto 133 145 const blobRef = { 134 146 $type: 'blob', 135 147 ref: CID.parse(TEST_CID), ··· 150 162 }) 151 163 152 164 test('prioritizes checking IPLD link first', () => { 153 - // Direct $link takes precedence 154 165 const directLink = { $link: TEST_CID } 155 166 expect(extractBlobCid(directLink)).toBe(TEST_CID) 156 167 }) ··· 167 178 expect(extractBlobCid(blobRef)).toBe(cidV1) 168 179 }) 169 180 }) 181 + 182 + const TEST_CID_BASE = 'bafkreid7ybejd5s2vv2j7d4aajjlmdgazguemcnuliiyfn6coxpwp2mi6y' 183 + 184 + function createMockBlobRef(cidSuffix: string = '', size: number = 100, mimeType: string = 'text/plain'): BlobRef { 185 + const cidString = TEST_CID_BASE 186 + return new BlobRef(CID.parse(cidString), mimeType, size) 187 + } 188 + 189 + function createFsFile( 190 + name: string, 191 + options: { mimeType?: string; size?: number; encoding?: 'gzip'; base64?: boolean } = {} 192 + ): FsEntry { 193 + const { mimeType = 'text/plain', size = 100, encoding, base64 } = options 194 + const file: $Typed<FsFile, 'place.wisp.fs#file'> = { 195 + $type: 'place.wisp.fs#file', 196 + type: 'file', 197 + blob: createMockBlobRef(name.replace(/[^a-z0-9]/gi, ''), size, mimeType), 198 + ...(encoding && { encoding }), 199 + ...(mimeType && { mimeType }), 200 + ...(base64 && { base64 }), 201 + } 202 + return { name, node: file } 203 + } 204 + 205 + function createFsDirectory(name: string, entries: FsEntry[]): FsEntry { 206 + const dir: $Typed<FsDirectory, 'place.wisp.fs#directory'> = { 207 + $type: 'place.wisp.fs#directory', 208 + type: 'directory', 209 + entries, 210 + } 211 + return { name, node: dir } 212 + } 213 + 214 + function createFsSubfs(name: string, subject: string, flat: boolean = true): FsEntry { 215 + const subfs: $Typed<FsSubfs, 'place.wisp.fs#subfs'> = { 216 + $type: 'place.wisp.fs#subfs', 217 + type: 'subfs', 218 + subject, 219 + flat, 220 + } 221 + return { name, node: subfs } 222 + } 223 + 224 + function createFsRootDirectory(entries: FsEntry[]): FsDirectory { 225 + return { 226 + $type: 'place.wisp.fs#directory', 227 + type: 'directory', 228 + entries, 229 + } 230 + } 231 + 232 + function createFsRecord(site: string, entries: FsEntry[], fileCount?: number): WispFsRecord { 233 + return { 234 + $type: 'place.wisp.fs', 235 + site, 236 + root: createFsRootDirectory(entries), 237 + ...(fileCount !== undefined && { fileCount }), 238 + createdAt: new Date().toISOString(), 239 + } 240 + } 241 + 242 + function createSubfsFile( 243 + name: string, 244 + options: { mimeType?: string; size?: number; encoding?: 'gzip'; base64?: boolean } = {} 245 + ): SubfsEntry { 246 + const { mimeType = 'text/plain', size = 100, encoding, base64 } = options 247 + const file: $Typed<SubfsFile, 'place.wisp.subfs#file'> = { 248 + $type: 'place.wisp.subfs#file', 249 + type: 'file', 250 + blob: createMockBlobRef(name.replace(/[^a-z0-9]/gi, ''), size, mimeType), 251 + ...(encoding && { encoding }), 252 + ...(mimeType && { mimeType }), 253 + ...(base64 && { base64 }), 254 + } 255 + return { name, node: file } 256 + } 257 + 258 + function createSubfsDirectory(name: string, entries: SubfsEntry[]): SubfsEntry { 259 + const dir: $Typed<SubfsDirectory, 'place.wisp.subfs#directory'> = { 260 + $type: 'place.wisp.subfs#directory', 261 + type: 'directory', 262 + entries, 263 + } 264 + return { name, node: dir } 265 + } 266 + 267 + function createSubfsSubfs(name: string, subject: string): SubfsEntry { 268 + const subfs: $Typed<SubfsSubfs, 'place.wisp.subfs#subfs'> = { 269 + $type: 'place.wisp.subfs#subfs', 270 + type: 'subfs', 271 + subject, 272 + } 273 + return { name, node: subfs } 274 + } 275 + 276 + function createSubfsRootDirectory(entries: SubfsEntry[]): SubfsDirectory { 277 + return { 278 + $type: 'place.wisp.subfs#directory', 279 + type: 'directory', 280 + entries, 281 + } 282 + } 283 + 284 + function createSubfsRecord(entries: SubfsEntry[], fileCount?: number): SubfsRecord { 285 + return { 286 + $type: 'place.wisp.subfs', 287 + root: createSubfsRootDirectory(entries), 288 + ...(fileCount !== undefined && { fileCount }), 289 + createdAt: new Date().toISOString(), 290 + } 291 + } 292 + 293 + describe('extractSubfsUris', () => { 294 + test('extracts subfs URIs from flat directory structure', () => { 295 + const subfsUri = 'at://did:plc:test/place.wisp.subfs/a' 296 + const dir = createFsRootDirectory([ 297 + createFsSubfs('a', subfsUri), 298 + createFsFile('file.txt'), 299 + ]) 300 + 301 + const uris = extractSubfsUris(dir) 302 + 303 + expect(uris).toHaveLength(1) 304 + expect(uris[0]).toEqual({ uri: subfsUri, path: 'a' }) 305 + }) 306 + 307 + test('extracts subfs URIs from nested directory structure', () => { 308 + const subfsAUri = 'at://did:plc:test/place.wisp.subfs/a' 309 + const subfsBUri = 'at://did:plc:test/place.wisp.subfs/b' 310 + 311 + const dir = createFsRootDirectory([ 312 + createFsSubfs('a', subfsAUri), 313 + createFsDirectory('nested', [ 314 + createFsSubfs('b', subfsBUri), 315 + createFsFile('file.txt'), 316 + ]), 317 + ]) 318 + 319 + const uris = extractSubfsUris(dir) 320 + 321 + expect(uris).toHaveLength(2) 322 + expect(uris).toContainEqual({ uri: subfsAUri, path: 'a' }) 323 + expect(uris).toContainEqual({ uri: subfsBUri, path: 'nested/b' }) 324 + }) 325 + 326 + test('returns empty array when no subfs nodes exist', () => { 327 + const dir = createFsRootDirectory([ 328 + createFsFile('file1.txt'), 329 + createFsDirectory('dir', [createFsFile('file2.txt')]), 330 + ]) 331 + 332 + const uris = extractSubfsUris(dir) 333 + expect(uris).toHaveLength(0) 334 + }) 335 + 336 + test('handles deeply nested subfs', () => { 337 + const subfsUri = 'at://did:plc:test/place.wisp.subfs/deep' 338 + const dir = createFsRootDirectory([ 339 + createFsDirectory('a', [ 340 + createFsDirectory('b', [ 341 + createFsDirectory('c', [ 342 + createFsSubfs('deep', subfsUri), 343 + ]), 344 + ]), 345 + ]), 346 + ]) 347 + 348 + const uris = extractSubfsUris(dir) 349 + 350 + expect(uris).toHaveLength(1) 351 + expect(uris[0]).toEqual({ uri: subfsUri, path: 'a/b/c/deep' }) 352 + }) 353 + }) 354 + 355 + describe('expandSubfsNodes caching', () => { 356 + test('cache map is populated after expansion', async () => { 357 + const subfsCache = new Map<string, SubfsRecord | null>() 358 + const dir = createFsRootDirectory([createFsFile('file.txt')]) 359 + 360 + const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 361 + 362 + expect(subfsCache.size).toBe(0) 363 + expect(result.entries).toHaveLength(1) 364 + expect(result.entries[0]?.name).toBe('file.txt') 365 + }) 366 + 367 + test('cache is passed through recursion depths', async () => { 368 + const subfsCache = new Map<string, SubfsRecord | null>() 369 + const mockSubfsUri = 'at://did:plc:test/place.wisp.subfs/cached' 370 + const mockRecord = createSubfsRecord([createSubfsFile('cached-file.txt')]) 371 + subfsCache.set(mockSubfsUri, mockRecord) 372 + 373 + const dir = createFsRootDirectory([createFsSubfs('cached', mockSubfsUri)]) 374 + const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 375 + 376 + expect(subfsCache.has(mockSubfsUri)).toBe(true) 377 + expect(result.entries).toHaveLength(1) 378 + expect(result.entries[0]?.name).toBe('cached-file.txt') 379 + }) 380 + 381 + test('pre-populated cache prevents re-fetching', async () => { 382 + const subfsCache = new Map<string, SubfsRecord | null>() 383 + const subfsAUri = 'at://did:plc:test/place.wisp.subfs/a' 384 + const subfsBUri = 'at://did:plc:test/place.wisp.subfs/b' 385 + 386 + subfsCache.set(subfsAUri, createSubfsRecord([createSubfsSubfs('b', subfsBUri)])) 387 + subfsCache.set(subfsBUri, createSubfsRecord([createSubfsFile('final.txt')])) 388 + 389 + const dir = createFsRootDirectory([createFsSubfs('a', subfsAUri)]) 390 + const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 391 + 392 + expect(result.entries).toHaveLength(1) 393 + expect(result.entries[0]?.name).toBe('final.txt') 394 + }) 395 + 396 + test('diamond dependency uses cache for shared reference', async () => { 397 + const subfsCache = new Map<string, SubfsRecord | null>() 398 + const subfsAUri = 'at://did:plc:test/place.wisp.subfs/a' 399 + const subfsBUri = 'at://did:plc:test/place.wisp.subfs/b' 400 + const subfsCUri = 'at://did:plc:test/place.wisp.subfs/c' 401 + 402 + subfsCache.set(subfsAUri, createSubfsRecord([createSubfsSubfs('c', subfsCUri)])) 403 + subfsCache.set(subfsBUri, createSubfsRecord([createSubfsSubfs('c', subfsCUri)])) 404 + subfsCache.set(subfsCUri, createSubfsRecord([createSubfsFile('shared.txt')])) 405 + 406 + const dir = createFsRootDirectory([ 407 + createFsSubfs('a', subfsAUri), 408 + createFsSubfs('b', subfsBUri), 409 + ]) 410 + const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 411 + 412 + expect(result.entries.filter(e => e.name === 'shared.txt')).toHaveLength(2) 413 + }) 414 + 415 + test('handles null records in cache gracefully', async () => { 416 + const subfsCache = new Map<string, SubfsRecord | null>() 417 + const subfsUri = 'at://did:plc:test/place.wisp.subfs/missing' 418 + subfsCache.set(subfsUri, null) 419 + 420 + const dir = createFsRootDirectory([ 421 + createFsFile('file.txt'), 422 + createFsSubfs('missing', subfsUri), 423 + ]) 424 + const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 425 + 426 + expect(result.entries.some(e => e.name === 'file.txt')).toBe(true) 427 + expect(result.entries.some(e => e.name === 'missing')).toBe(true) 428 + }) 429 + 430 + test('non-flat subfs merge creates directory instead of hoisting', async () => { 431 + const subfsCache = new Map<string, SubfsRecord | null>() 432 + const subfsUri = 'at://did:plc:test/place.wisp.subfs/nested' 433 + subfsCache.set(subfsUri, createSubfsRecord([createSubfsFile('nested-file.txt')])) 434 + 435 + const dir = createFsRootDirectory([ 436 + createFsFile('root.txt'), 437 + createFsSubfs('subdir', subfsUri, false), 438 + ]) 439 + const result = await expandSubfsNodes(dir, 'https://pds.example.com', 0, subfsCache) 440 + 441 + expect(result.entries).toHaveLength(2) 442 + 443 + const rootFile = result.entries.find(e => e.name === 'root.txt') 444 + expect(rootFile).toBeDefined() 445 + 446 + const subdir = result.entries.find(e => e.name === 'subdir') 447 + expect(subdir).toBeDefined() 448 + 449 + if (subdir && 'entries' in subdir.node) { 450 + expect(subdir.node.type).toBe('directory') 451 + expect(subdir.node.entries).toHaveLength(1) 452 + expect(subdir.node.entries[0]?.name).toBe('nested-file.txt') 453 + } 454 + }) 455 + }) 456 + 457 + describe('WispFsRecord mock builders', () => { 458 + test('createFsRecord creates valid record structure', () => { 459 + const record = createFsRecord('my-site', [ 460 + createFsFile('index.html', { mimeType: 'text/html' }), 461 + createFsDirectory('assets', [ 462 + createFsFile('style.css', { mimeType: 'text/css' }), 463 + ]), 464 + ]) 465 + 466 + expect(record.$type).toBe('place.wisp.fs') 467 + expect(record.site).toBe('my-site') 468 + expect(record.root.type).toBe('directory') 469 + expect(record.root.entries).toHaveLength(2) 470 + expect(record.createdAt).toBeDefined() 471 + }) 472 + 473 + test('createFsFile creates valid file entry', () => { 474 + const entry = createFsFile('test.html', { mimeType: 'text/html', size: 500 }) 475 + 476 + expect(entry.name).toBe('test.html') 477 + 478 + const file = entry.node 479 + if ('blob' in file) { 480 + expect(file.$type).toBe('place.wisp.fs#file') 481 + expect(file.type).toBe('file') 482 + expect(file.blob).toBeDefined() 483 + expect(file.mimeType).toBe('text/html') 484 + } 485 + }) 486 + 487 + test('createFsFile with gzip encoding', () => { 488 + const entry = createFsFile('bundle.js', { mimeType: 'application/javascript', encoding: 'gzip' }) 489 + 490 + const file = entry.node 491 + if ('encoding' in file) { 492 + expect(file.encoding).toBe('gzip') 493 + } 494 + }) 495 + 496 + test('createFsFile with base64 flag', () => { 497 + const entry = createFsFile('data.bin', { base64: true }) 498 + 499 + const file = entry.node 500 + if ('base64' in file) { 501 + expect(file.base64).toBe(true) 502 + } 503 + }) 504 + 505 + test('createFsDirectory creates valid directory entry', () => { 506 + const entry = createFsDirectory('assets', [ 507 + createFsFile('file1.txt'), 508 + createFsFile('file2.txt'), 509 + ]) 510 + 511 + expect(entry.name).toBe('assets') 512 + 513 + const dir = entry.node 514 + if ('entries' in dir) { 515 + expect(dir.$type).toBe('place.wisp.fs#directory') 516 + expect(dir.type).toBe('directory') 517 + expect(dir.entries).toHaveLength(2) 518 + } 519 + }) 520 + 521 + test('createFsSubfs creates valid subfs entry with flat=true', () => { 522 + const entry = createFsSubfs('external', 'at://did:plc:test/place.wisp.subfs/ext') 523 + 524 + expect(entry.name).toBe('external') 525 + 526 + const subfs = entry.node 527 + if ('subject' in subfs) { 528 + expect(subfs.$type).toBe('place.wisp.fs#subfs') 529 + expect(subfs.type).toBe('subfs') 530 + expect(subfs.subject).toBe('at://did:plc:test/place.wisp.subfs/ext') 531 + expect(subfs.flat).toBe(true) 532 + } 533 + }) 534 + 535 + test('createFsSubfs creates valid subfs entry with flat=false', () => { 536 + const entry = createFsSubfs('external', 'at://did:plc:test/place.wisp.subfs/ext', false) 537 + 538 + const subfs = entry.node 539 + if ('subject' in subfs) { 540 + expect(subfs.flat).toBe(false) 541 + } 542 + }) 543 + 544 + test('createFsRecord with fileCount', () => { 545 + const record = createFsRecord('my-site', [createFsFile('index.html')], 1) 546 + expect(record.fileCount).toBe(1) 547 + }) 548 + }) 549 + 550 + describe('SubfsRecord mock builders', () => { 551 + test('createSubfsRecord creates valid record structure', () => { 552 + const record = createSubfsRecord([ 553 + createSubfsFile('file1.txt'), 554 + createSubfsDirectory('nested', [ 555 + createSubfsFile('file2.txt'), 556 + ]), 557 + ]) 558 + 559 + expect(record.$type).toBe('place.wisp.subfs') 560 + expect(record.root.type).toBe('directory') 561 + expect(record.root.entries).toHaveLength(2) 562 + expect(record.createdAt).toBeDefined() 563 + }) 564 + 565 + test('createSubfsFile creates valid file entry', () => { 566 + const entry = createSubfsFile('data.json', { mimeType: 'application/json', size: 1024 }) 567 + 568 + expect(entry.name).toBe('data.json') 569 + 570 + const file = entry.node 571 + if ('blob' in file) { 572 + expect(file.$type).toBe('place.wisp.subfs#file') 573 + expect(file.type).toBe('file') 574 + expect(file.blob).toBeDefined() 575 + expect(file.mimeType).toBe('application/json') 576 + } 577 + }) 578 + 579 + test('createSubfsDirectory creates valid directory entry', () => { 580 + const entry = createSubfsDirectory('subdir', [createSubfsFile('inner.txt')]) 581 + 582 + expect(entry.name).toBe('subdir') 583 + 584 + const dir = entry.node 585 + if ('entries' in dir) { 586 + expect(dir.$type).toBe('place.wisp.subfs#directory') 587 + expect(dir.type).toBe('directory') 588 + expect(dir.entries).toHaveLength(1) 589 + } 590 + }) 591 + 592 + test('createSubfsSubfs creates valid nested subfs entry', () => { 593 + const entry = createSubfsSubfs('deeper', 'at://did:plc:test/place.wisp.subfs/deeper') 594 + 595 + expect(entry.name).toBe('deeper') 596 + 597 + const subfs = entry.node 598 + if ('subject' in subfs) { 599 + expect(subfs.$type).toBe('place.wisp.subfs#subfs') 600 + expect(subfs.type).toBe('subfs') 601 + expect(subfs.subject).toBe('at://did:plc:test/place.wisp.subfs/deeper') 602 + expect('flat' in subfs).toBe(false) 603 + } 604 + }) 605 + 606 + test('createSubfsRecord with fileCount', () => { 607 + const record = createSubfsRecord([createSubfsFile('file.txt')], 1) 608 + expect(record.fileCount).toBe(1) 609 + }) 610 + }) 611 + 612 + describe('extractBlobCid with typed mock data', () => { 613 + test('extracts CID from FsFile blob', () => { 614 + const entry = createFsFile('test.txt') 615 + const file = entry.node 616 + 617 + if ('blob' in file) { 618 + const cid = extractBlobCid(file.blob) 619 + expect(cid).toBeDefined() 620 + expect(cid).toContain('bafkrei') 621 + } 622 + }) 623 + 624 + test('extracts CID from SubfsFile blob', () => { 625 + const entry = createSubfsFile('test.txt') 626 + const file = entry.node 627 + 628 + if ('blob' in file) { 629 + const cid = extractBlobCid(file.blob) 630 + expect(cid).toBeDefined() 631 + expect(cid).toContain('bafkrei') 632 + } 633 + }) 634 + })
+33 -13
apps/hosting-service/src/lib/utils.ts
··· 149 149 /** 150 150 * Extract all subfs URIs from a directory tree with their mount paths 151 151 */ 152 - function extractSubfsUris(directory: Directory, currentPath: string = ''): Array<{ uri: string; path: string }> { 152 + export function extractSubfsUris(directory: Directory, currentPath: string = ''): Array<{ uri: string; path: string }> { 153 153 const uris: Array<{ uri: string; path: string }> = []; 154 154 155 155 for (const entry of directory.entries) { ··· 209 209 * Replace subfs nodes in a directory tree with their actual content 210 210 * Subfs entries are "merged" - their root entries are hoisted into the parent directory 211 211 * This function is recursive - it will keep expanding until no subfs nodes remain 212 + * Uses a cache to avoid re-fetching the same subfs records across recursion depths 212 213 */ 213 - async function expandSubfsNodes(directory: Directory, pdsEndpoint: string, depth: number = 0): Promise<Directory> { 214 + export async function expandSubfsNodes( 215 + directory: Directory, 216 + pdsEndpoint: string, 217 + depth: number = 0, 218 + subfsCache: Map<string, SubfsRecord | null> = new Map() 219 + ): Promise<Directory> { 214 220 const MAX_DEPTH = 10; // Prevent infinite loops 215 221 216 222 if (depth >= MAX_DEPTH) { ··· 226 232 return directory; 227 233 } 228 234 229 - console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs records, fetching...`); 235 + // Filter to only URIs we haven't fetched yet 236 + const uncachedUris = subfsUris.filter(({ uri }) => !subfsCache.has(uri)); 230 237 231 - // Fetch all subfs records in parallel 232 - const subfsRecords = await Promise.all( 233 - subfsUris.map(async ({ uri, path }) => { 234 - const record = await fetchSubfsRecord(uri, pdsEndpoint); 235 - return { record, path }; 236 - }) 237 - ); 238 + if (uncachedUris.length > 0) { 239 + console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs references, fetching ${uncachedUris.length} new records (${subfsUris.length - uncachedUris.length} cached)...`); 238 240 239 - // Build a map of path -> root entries to merge 241 + // Fetch only uncached subfs records in parallel 242 + const fetchedRecords = await Promise.all( 243 + uncachedUris.map(async ({ uri }) => { 244 + const record = await fetchSubfsRecord(uri, pdsEndpoint); 245 + return { uri, record }; 246 + }) 247 + ); 248 + 249 + // Add fetched records to cache 250 + for (const { uri, record } of fetchedRecords) { 251 + subfsCache.set(uri, record); 252 + } 253 + } else { 254 + console.log(`[Depth ${depth}] Found ${subfsUris.length} subfs references, all cached`); 255 + } 256 + 257 + // Build a map of path -> root entries to merge using the cache 240 258 // Note: SubFS entries are compatible with FS entries at runtime 241 259 const subfsMap = new Map<string, Entry[]>(); 242 - for (const { record, path } of subfsRecords) { 260 + for (const { uri, path } of subfsUris) { 261 + const record = subfsCache.get(uri); 243 262 if (record && record.root && record.root.entries) { 244 263 subfsMap.set(path, record.root.entries as unknown as Entry[]); 245 264 } ··· 307 326 }; 308 327 309 328 // Recursively expand any remaining subfs nodes (e.g., nested subfs inside parent subfs) 310 - return expandSubfsNodes(partiallyExpanded, pdsEndpoint, depth + 1); 329 + // Pass the cache to avoid re-fetching records 330 + return expandSubfsNodes(partiallyExpanded, pdsEndpoint, depth + 1, subfsCache); 311 331 } 312 332 313 333