1- import type { DucExternalFile , DucExternalFiles , ExternalFileId , ExternalFileRevision } from "./types" ;
1+ import type { DucExternalFile , DucExternalFiles , ExternalFileId , ExternalFileLoaded , ResolvedFileData , ExternalFilesData } from "./types" ;
22import { wasmGetExternalFile , wasmListExternalFiles } from "./wasm" ;
33
44export type LazyFileMetadata = {
@@ -19,7 +19,7 @@ export type LazyFileMetadata = {
1919export class LazyExternalFileStore {
2020 private buffer : Uint8Array | null ;
2121 private metadataCache : Map < string , LazyFileMetadata > | null = null ;
22- private runtimeFiles : Map < string , DucExternalFile > = new Map ( ) ;
22+ private runtimeFiles : Map < string , ExternalFileLoaded > = new Map ( ) ;
2323
2424 constructor ( buffer : Uint8Array ) {
2525 this . buffer = buffer ;
@@ -80,26 +80,30 @@ export class LazyExternalFileStore {
8080 }
8181
8282 /** Fetch the full file (including data blobs for all revisions) for a specific file. */
83- getFile ( fileId : string ) : DucExternalFile | null {
83+ getFile ( fileId : string ) : ExternalFileLoaded | null {
8484 const rt = this . runtimeFiles . get ( fileId ) ;
8585 if ( rt ) return rt ;
8686
8787 if ( ! this . buffer ) return null ;
8888 const result = wasmGetExternalFile ( this . buffer , fileId ) ;
8989 if ( ! result ) return null ;
9090
91- return result as DucExternalFile ;
91+ return result as ExternalFileLoaded ;
9292 }
9393
9494 /** Get the active revision data for a specific file. */
95- getFileData ( fileId : string ) : ExternalFileRevision | null {
96- const file = this . getFile ( fileId ) ;
97- if ( ! file ) return null ;
98- return file . revisions [ file . activeRevisionId ] ?? null ;
95+ getFileData ( fileId : string ) : ResolvedFileData | null {
96+ const loaded = this . getFile ( fileId ) ;
97+ if ( ! loaded ) return null ;
98+ const meta = loaded . revisions [ loaded . activeRevisionId ] ;
99+ if ( ! meta ) return null ;
100+ const dataBlob = loaded . data [ loaded . activeRevisionId ] ;
101+ if ( ! dataBlob ) return null ;
102+ return { data : dataBlob , mimeType : meta . mimeType } ;
99103 }
100104
101105 /** Fetch active revision data and return a copy of the data buffer (safe for transfer). */
102- getFileDataCopy ( fileId : string ) : ExternalFileRevision | null {
106+ getFileDataCopy ( fileId : string ) : ResolvedFileData | null {
103107 const data = this . getFileData ( fileId ) ;
104108 if ( ! data ) return null ;
105109 return {
@@ -109,66 +113,119 @@ export class LazyExternalFileStore {
109113 }
110114
111115 /** Add a file at runtime (not persisted in .duc until next serialize). */
112- addRuntimeFile ( fileId : string , file : DucExternalFile ) : void {
113- this . runtimeFiles . set ( fileId , file ) ;
116+ addRuntimeFile ( fileId : string , file : DucExternalFile , data : Record < string , Uint8Array > ) : void {
117+ this . runtimeFiles . set ( fileId , { ... file , data } ) ;
114118 }
115119
116120 /** Remove a runtime file. */
117121 removeRuntimeFile ( fileId : string ) : boolean {
118122 return this . runtimeFiles . delete ( fileId ) ;
119123 }
120124
121- /** Export all files eagerly as a DucExternalFiles record. */
125+ /** Export all files metadata as a DucExternalFiles record. */
122126 toExternalFiles ( ) : DucExternalFiles {
123127 const result : DucExternalFiles = { } ;
124128
125129 if ( this . buffer ) {
126130 for ( const [ id ] of this . getMetadataMap ( ) ) {
127- const file = this . getFile ( id ) ;
128- if ( file ) {
131+ const loaded = this . getFile ( id ) ;
132+ if ( loaded ) {
133+ const { data : _ , ...file } = loaded ;
129134 result [ id as ExternalFileId ] = file ;
130135 }
131136 }
132137 }
133138
134- for ( const [ id , file ] of this . runtimeFiles ) {
139+ for ( const [ id , loaded ] of this . runtimeFiles ) {
140+ const { data : _ , ...file } = loaded ;
135141 result [ id as ExternalFileId ] = file ;
136142 }
137143
138144 return result ;
139145 }
140146
141- /** Merge files from another source. Adds missing files and merges new revisions into existing ones. */
142- mergeFiles ( files : DucExternalFiles ) : void {
143- for ( const [ id , file ] of Object . entries ( files ) ) {
144- if ( ! this . has ( id ) ) {
145- this . runtimeFiles . set ( id , file ) ;
146- continue ;
147+ /** Export all revision data blobs as an ExternalFilesData record. */
148+ toExternalFilesData ( ) : ExternalFilesData {
149+ const result : ExternalFilesData = { } ;
150+
151+ if ( this . buffer ) {
152+ for ( const [ id ] of this . getMetadataMap ( ) ) {
153+ const loaded = this . getFile ( id ) ;
154+ if ( loaded ) {
155+ for ( const [ revId , blob ] of Object . entries ( loaded . data ) ) {
156+ result [ revId ] = blob ;
157+ }
158+ }
147159 }
160+ }
148161
162+ for ( const [ , loaded ] of this . runtimeFiles ) {
163+ for ( const [ revId , blob ] of Object . entries ( loaded . data ) ) {
164+ result [ revId ] = blob ;
165+ }
166+ }
167+
168+ return result ;
169+ }
170+
171+ /** Merge files from another source. Adds missing files and merges new revisions into existing ones. */
172+ mergeFiles ( files : DucExternalFiles , filesData ?: ExternalFilesData ) : void {
173+ for ( const [ id , file ] of Object . entries ( files ) ) {
149174 const existing = this . runtimeFiles . get ( id ) ?? this . getFile ( id ) ;
175+
150176 if ( ! existing ) {
151- this . runtimeFiles . set ( id , file ) ;
177+ const dataMap : Record < string , Uint8Array > = { } ;
178+ if ( filesData ) {
179+ for ( const revId of Object . keys ( file . revisions ) ) {
180+ if ( filesData [ revId ] ) {
181+ dataMap [ revId ] = filesData [ revId ] ;
182+ }
183+ }
184+ }
185+ this . runtimeFiles . set ( id , { ...file , data : dataMap } ) ;
152186 continue ;
153187 }
154188
155- // Merge: add any new revisions that don't exist yet, and update metadata
156189 let merged = false ;
157190 const mergedRevisions = { ...existing . revisions } ;
191+ const mergedData = { ...existing . data } ;
158192 for ( const [ revId , rev ] of Object . entries ( file . revisions ) ) {
159193 if ( ! mergedRevisions [ revId ] ) {
194+ mergedRevisions [ revId ] = rev ;
195+ if ( filesData ?. [ revId ] ) {
196+ mergedData [ revId ] = filesData [ revId ] ;
197+ }
198+ merged = true ;
199+ continue ;
200+ }
201+
202+ if ( filesData ?. [ revId ] && mergedData [ revId ] !== filesData [ revId ] ) {
203+ mergedData [ revId ] = filesData [ revId ] ;
204+ merged = true ;
205+ }
206+
207+ if (
208+ rev . sizeBytes !== mergedRevisions [ revId ] . sizeBytes ||
209+ rev . lastRetrieved !== mergedRevisions [ revId ] . lastRetrieved ||
210+ rev . created !== mergedRevisions [ revId ] . created ||
211+ rev . mimeType !== mergedRevisions [ revId ] . mimeType ||
212+ rev . sourceName !== mergedRevisions [ revId ] . sourceName ||
213+ rev . message !== mergedRevisions [ revId ] . message
214+ ) {
160215 mergedRevisions [ revId ] = rev ;
161216 merged = true ;
162217 }
163218 }
164219
165- if ( merged || file . updated > existing . updated ) {
220+ const activeRevisionChanged = file . activeRevisionId !== existing . activeRevisionId ;
221+ if ( merged || activeRevisionChanged || file . updated > existing . updated ) {
166222 this . runtimeFiles . set ( id , {
167223 ...existing ,
168224 activeRevisionId : file . activeRevisionId ,
169225 updated : Math . max ( file . updated , existing . updated ) ,
170226 version : Math . max ( file . version ?? 0 , existing . version ?? 0 ) ,
171227 revisions : mergedRevisions ,
228+ data : mergedData ,
172229 } ) ;
173230 }
174231 }
0 commit comments