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

add flat: boolean to subfs definition in place.wisp.fs to define if to merge flat or as a subdirectory, handle this logic better across backend and hosting service

Changed files
+74 -20
hosting-service
src
lexicon
types
place
lib
lexicons
src
lexicons
types
place
lib
routes
+8 -3
hosting-service/src/lexicon/lexicons.ts
··· 118 type: 'string', 119 format: 'at-uri', 120 description: 121 - 'AT-URI pointing to a place.wisp.subfs record containing this subtree', 122 }, 123 }, 124 }, ··· 131 main: { 132 type: 'record', 133 description: 134 - 'Virtual filesystem manifest within a place.wisp.fs record', 135 record: { 136 type: 'object', 137 required: ['root', 'createdAt'], ··· 230 type: 'string', 231 format: 'at-uri', 232 description: 233 - 'AT-URI pointing to another place.wisp.subfs record for nested subtrees', 234 }, 235 }, 236 },
··· 118 type: 'string', 119 format: 'at-uri', 120 description: 121 + 'AT-URI pointing to a place.wisp.subfs record containing this subtree.', 122 + }, 123 + flat: { 124 + type: 'boolean', 125 + description: 126 + "If true, the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false (default), the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure.", 127 }, 128 }, 129 }, ··· 136 main: { 137 type: 'record', 138 description: 139 + 'Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.', 140 record: { 141 type: 'object', 142 required: ['root', 'createdAt'], ··· 235 type: 'string', 236 format: 'at-uri', 237 description: 238 + "AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures.", 239 }, 240 }, 241 },
+3 -1
hosting-service/src/lexicon/types/place/wisp/fs.ts
··· 93 export interface Subfs { 94 $type?: 'place.wisp.fs#subfs' 95 type: 'subfs' 96 - /** AT-URI pointing to a place.wisp.subfs record containing this subtree */ 97 subject: string 98 } 99 100 const hashSubfs = 'subfs'
··· 93 export interface Subfs { 94 $type?: 'place.wisp.fs#subfs' 95 type: 'subfs' 96 + /** AT-URI pointing to a place.wisp.subfs record containing this subtree. */ 97 subject: string 98 + /** If true, the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false (default), the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure. */ 99 + flat?: boolean 100 } 101 102 const hashSubfs = 'subfs'
+1 -1
hosting-service/src/lexicon/types/place/wisp/subfs.ts
··· 92 export interface Subfs { 93 $type?: 'place.wisp.subfs#subfs' 94 type: 'subfs' 95 - /** AT-URI pointing to another place.wisp.subfs record for nested subtrees */ 96 subject: string 97 } 98
··· 92 export interface Subfs { 93 $type?: 'place.wisp.subfs#subfs' 94 type: 'subfs' 95 + /** AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures. */ 96 subject: string 97 } 98
+1
hosting-service/src/lib/firehose.ts
··· 82 evt.cid?.toString() 83 ) 84 } catch (err) { 85 this.log('Error handling event', { 86 did: evt.did, 87 event: evt.event,
··· 82 evt.cid?.toString() 83 ) 84 } catch (err) { 85 + console.error('Full error details:', err); 86 this.log('Error handling event', { 87 did: evt.did, 88 event: evt.event,
+43 -7
hosting-service/src/lib/utils.ts
··· 295 const node = entry.node; 296 297 if ('type' in node && node.type === 'subfs') { 298 - // Merge subfs entries into parent directory 299 const subfsEntries = subfsMap.get(fullPath); 300 if (subfsEntries) { 301 - console.log(`Merging subfs node at ${fullPath} (${subfsEntries.length} entries)`); 302 - // Recursively process the merged entries in case they contain nested subfs 303 - const processedEntries = replaceSubfsInEntries(subfsEntries, currentPath); 304 - result.push(...processedEntries); 305 } else { 306 // If fetch failed, skip this entry 307 console.warn(`Failed to fetch subfs at ${fullPath}, skipping`); ··· 491 492 // Download new/changed files concurrently - increased from 3 to 20 for much better performance 493 const downloadLimit = 20; 494 for (let i = 0; i < downloadTasks.length; i += downloadLimit) { 495 const batch = downloadTasks.slice(i, i + downloadLimit); 496 - await Promise.all(batch.map(task => task())); 497 if (downloadTasks.length > downloadLimit) { 498 - console.log(`[Cache Progress] Downloaded ${Math.min(i + downloadLimit, downloadTasks.length)}/${downloadTasks.length} files`); 499 } 500 } 501 } 502 ··· 555 } 556 557 const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`; 558 559 // Allow up to 500MB per file blob, with 5 minute timeout 560 let content = await safeFetchBlob(blobUrl, { maxSize: 500 * 1024 * 1024, timeout: 300000 });
··· 295 const node = entry.node; 296 297 if ('type' in node && node.type === 'subfs') { 298 + // Check if this is a flat merge or subdirectory merge (default to flat if not specified) 299 + const subfsNode = node as any; 300 + const isFlat = subfsNode.flat !== false; // Default to true 301 const subfsEntries = subfsMap.get(fullPath); 302 + 303 if (subfsEntries) { 304 + console.log(`Merging subfs node at ${fullPath} (${subfsEntries.length} entries, flat: ${isFlat})`); 305 + 306 + if (isFlat) { 307 + // Flat merge: hoist entries directly into parent directory 308 + const processedEntries = replaceSubfsInEntries(subfsEntries, currentPath); 309 + result.push(...processedEntries); 310 + } else { 311 + // Subdirectory merge: create a directory with the subfs node's name 312 + const processedEntries = replaceSubfsInEntries(subfsEntries, fullPath); 313 + result.push({ 314 + name: entry.name, 315 + node: { 316 + type: 'directory', 317 + entries: processedEntries 318 + } 319 + }); 320 + } 321 } else { 322 // If fetch failed, skip this entry 323 console.warn(`Failed to fetch subfs at ${fullPath}, skipping`); ··· 507 508 // Download new/changed files concurrently - increased from 3 to 20 for much better performance 509 const downloadLimit = 20; 510 + let successCount = 0; 511 + let failureCount = 0; 512 + 513 for (let i = 0; i < downloadTasks.length; i += downloadLimit) { 514 const batch = downloadTasks.slice(i, i + downloadLimit); 515 + const results = await Promise.allSettled(batch.map(task => task())); 516 + 517 + // Count successes and failures 518 + results.forEach((result, index) => { 519 + if (result.status === 'fulfilled') { 520 + successCount++; 521 + } else { 522 + failureCount++; 523 + console.error(`[Cache] Failed to download file (continuing with others):`, result.reason); 524 + } 525 + }); 526 + 527 if (downloadTasks.length > downloadLimit) { 528 + console.log(`[Cache Progress] Downloaded ${Math.min(i + downloadLimit, downloadTasks.length)}/${downloadTasks.length} files (${failureCount} failed)`); 529 } 530 + } 531 + 532 + if (failureCount > 0) { 533 + console.warn(`[Cache] Completed with ${successCount} successful and ${failureCount} failed file downloads`); 534 } 535 } 536 ··· 589 } 590 591 const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`; 592 + 593 + console.log(`[Cache] Fetching blob for file: ${filePath}, CID: ${cid}`); 594 595 // Allow up to 500MB per file blob, with 5 minute timeout 596 let content = await safeFetchBlob(blobUrl, { maxSize: 500 * 1024 * 1024, timeout: 300000 });
+2 -1
lexicons/fs.json
··· 51 "required": ["type", "subject"], 52 "properties": { 53 "type": { "type": "string", "const": "subfs" }, 54 - "subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to a place.wisp.subfs record containing this subtree. When expanded, the subfs record's root entries are merged (flattened) into the parent directory - the subfs entry itself is removed and replaced by all entries from the referenced record's root. This allows splitting large directories across multiple records while maintaining a flat structure." } 55 } 56 } 57 }
··· 51 "required": ["type", "subject"], 52 "properties": { 53 "type": { "type": "string", "const": "subfs" }, 54 + "subject": { "type": "string", "format": "at-uri", "description": "AT-URI pointing to a place.wisp.subfs record containing this subtree." }, 55 + "flat": { "type": "boolean", "description": "If true, the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false (default), the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure." } 56 } 57 } 58 }
+8 -3
src/lexicons/lexicons.ts
··· 118 type: 'string', 119 format: 'at-uri', 120 description: 121 - 'AT-URI pointing to a place.wisp.subfs record containing this subtree', 122 }, 123 }, 124 }, ··· 131 main: { 132 type: 'record', 133 description: 134 - 'Virtual filesystem manifest within a place.wisp.fs record', 135 record: { 136 type: 'object', 137 required: ['root', 'createdAt'], ··· 230 type: 'string', 231 format: 'at-uri', 232 description: 233 - 'AT-URI pointing to another place.wisp.subfs record for nested subtrees', 234 }, 235 }, 236 },
··· 118 type: 'string', 119 format: 'at-uri', 120 description: 121 + 'AT-URI pointing to a place.wisp.subfs record containing this subtree.', 122 + }, 123 + flat: { 124 + type: 'boolean', 125 + description: 126 + "If true, the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false (default), the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure.", 127 }, 128 }, 129 }, ··· 136 main: { 137 type: 'record', 138 description: 139 + 'Virtual filesystem subtree referenced by place.wisp.fs records. When a subfs entry is expanded, its root entries are merged (flattened) into the parent directory, allowing large directories to be split across multiple records while maintaining a flat structure.', 140 record: { 141 type: 'object', 142 required: ['root', 'createdAt'], ··· 235 type: 'string', 236 format: 'at-uri', 237 description: 238 + "AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures.", 239 }, 240 }, 241 },
+3 -1
src/lexicons/types/place/wisp/fs.ts
··· 93 export interface Subfs { 94 $type?: 'place.wisp.fs#subfs' 95 type: 'subfs' 96 - /** AT-URI pointing to a place.wisp.subfs record containing this subtree */ 97 subject: string 98 } 99 100 const hashSubfs = 'subfs'
··· 93 export interface Subfs { 94 $type?: 'place.wisp.fs#subfs' 95 type: 'subfs' 96 + /** AT-URI pointing to a place.wisp.subfs record containing this subtree. */ 97 subject: string 98 + /** If true, the subfs record's root entries are merged (flattened) into the parent directory, replacing the subfs entry. If false (default), the subfs entries are placed in a subdirectory with the subfs entry's name. Flat merging is useful for splitting large directories across multiple records while maintaining a flat structure. */ 99 + flat?: boolean 100 } 101 102 const hashSubfs = 'subfs'
+1 -1
src/lexicons/types/place/wisp/subfs.ts
··· 92 export interface Subfs { 93 $type?: 'place.wisp.subfs#subfs' 94 type: 'subfs' 95 - /** AT-URI pointing to another place.wisp.subfs record for nested subtrees */ 96 subject: string 97 } 98
··· 92 export interface Subfs { 93 $type?: 'place.wisp.subfs#subfs' 94 type: 'subfs' 95 + /** AT-URI pointing to another place.wisp.subfs record for nested subtrees. When expanded, the referenced record's root entries are merged (flattened) into the parent directory, allowing recursive splitting of large directory structures. */ 96 subject: string 97 } 98
+2 -1
src/lib/wisp-utils.ts
··· 424 node: { 425 $type: 'place.wisp.fs#subfs' as const, 426 type: 'subfs' as const, 427 - subject: subfsUri 428 } 429 }; 430 }
··· 424 node: { 425 $type: 'place.wisp.fs#subfs' as const, 426 type: 'subfs' as const, 427 + subject: subfsUri, 428 + flat: false // Preserve directory structure 429 } 430 }; 431 }
+2 -1
src/routes/wisp.ts
··· 719 node: { 720 $type: 'place.wisp.fs#subfs' as const, 721 type: 'subfs' as const, 722 - subject: subfsUri 723 } 724 }); 725
··· 719 node: { 720 $type: 'place.wisp.fs#subfs' as const, 721 type: 'subfs' as const, 722 + subject: subfsUri, 723 + flat: true // Merge entries directly into parent (default, but explicit for clarity) 724 } 725 }); 726