@@ -11,6 +11,44 @@ import * as log from "./utils/logger"
1111export class CodeFilesAPI {
1212 private lastSnapshot = new Map < string , string > ( )
1313
14+ // Keep the snapshot aligned with the state Framer should expose, even while a remote mutation is in flight.
15+ private async withExpectedSnapshotPatch < T > (
16+ patch : {
17+ upserts ?: { fileName : string ; content : string } [ ]
18+ deletes ?: string [ ]
19+ } ,
20+ run : ( ) => Promise < T >
21+ ) : Promise < T > {
22+ const previousEntries = new Map < string , string | undefined > ( )
23+
24+ for ( const fileName of patch . deletes ?? [ ] ) {
25+ if ( ! previousEntries . has ( fileName ) ) {
26+ previousEntries . set ( fileName , this . lastSnapshot . get ( fileName ) )
27+ }
28+ this . lastSnapshot . delete ( fileName )
29+ }
30+
31+ for ( const entry of patch . upserts ?? [ ] ) {
32+ if ( ! previousEntries . has ( entry . fileName ) ) {
33+ previousEntries . set ( entry . fileName , this . lastSnapshot . get ( entry . fileName ) )
34+ }
35+ this . lastSnapshot . set ( entry . fileName , entry . content )
36+ }
37+
38+ try {
39+ return await run ( )
40+ } catch ( error ) {
41+ for ( const [ fileName , previousContent ] of previousEntries ) {
42+ if ( previousContent === undefined ) {
43+ this . lastSnapshot . delete ( fileName )
44+ } else {
45+ this . lastSnapshot . set ( fileName , previousContent )
46+ }
47+ }
48+ throw error
49+ }
50+ }
51+
1452 private async getCodeFilesWithCanonicalNames ( ) {
1553 // Always all files instead of single file calls.
1654 // The API internally does that anyways.
@@ -78,22 +116,12 @@ export class CodeFilesAPI {
78116
79117 async applyRemoteChange ( fileName : string , content : string , socket : WebSocket ) {
80118 const normalizedName = normalizeCodeFileName ( fileName )
81- const previousSnapshot = this . lastSnapshot . get ( normalizedName )
82-
83- // Update snapshot BEFORE upsert to prevent race with file subscription.
84- this . lastSnapshot . set ( normalizedName , content )
85-
86- let updatedAt : number | undefined
87- try {
88- updatedAt = await upsertFramerFile ( normalizedName , content )
89- } catch ( error ) {
90- if ( previousSnapshot !== undefined ) {
91- this . lastSnapshot . set ( normalizedName , previousSnapshot )
92- } else {
93- this . lastSnapshot . delete ( normalizedName )
94- }
95- throw error
96- }
119+ const updatedAt = await this . withExpectedSnapshotPatch (
120+ {
121+ upserts : [ { fileName : normalizedName , content } ] ,
122+ } ,
123+ async ( ) => await upsertFramerFile ( normalizedName , content )
124+ )
97125
98126 // Send file-synced message with timestamp
99127 const syncTimestamp = updatedAt ?? Date . now ( )
@@ -110,8 +138,15 @@ export class CodeFilesAPI {
110138 }
111139
112140 async applyRemoteDelete ( fileName : string ) {
113- await deleteFramerFile ( fileName )
114- this . lastSnapshot . delete ( normalizeCodeFileName ( fileName ) )
141+ const normalizedName = normalizeCodeFileName ( fileName )
142+ await this . withExpectedSnapshotPatch (
143+ {
144+ deletes : [ normalizedName ] ,
145+ } ,
146+ async ( ) => {
147+ await deleteFramerFile ( normalizedName )
148+ }
149+ )
115150 }
116151
117152 async readCurrentContent ( fileName : string ) {
@@ -209,16 +244,16 @@ export class CodeFilesAPI {
209244 return false
210245 }
211246
212- const previousSourceSnapshot = this . lastSnapshot . get ( sourceFileName )
213- const previousTargetSnapshot = this . lastSnapshot . get ( targetFileName )
214- const renamedContent = previousSourceSnapshot ?? existing . content
215-
216- // Update snapshot BEFORE rename to prevent race with file subscription.
217- this . lastSnapshot . delete ( sourceFileName )
218- this . lastSnapshot . set ( targetFileName , renamedContent )
247+ const content = this . lastSnapshot . get ( sourceFileName ) ?? existing . content
219248
220249 try {
221- await existing . rename ( targetFileName )
250+ await this . withExpectedSnapshotPatch (
251+ {
252+ upserts : [ { fileName : targetFileName , content } ] ,
253+ deletes : [ sourceFileName ] ,
254+ } ,
255+ async ( ) => await existing . rename ( targetFileName )
256+ )
222257 socket . send (
223258 JSON . stringify ( {
224259 type : "file-synced" ,
@@ -228,16 +263,6 @@ export class CodeFilesAPI {
228263 )
229264 return true
230265 } catch ( err ) {
231- if ( previousSourceSnapshot !== undefined ) {
232- this . lastSnapshot . set ( sourceFileName , previousSourceSnapshot )
233- }
234-
235- if ( previousTargetSnapshot !== undefined ) {
236- this . lastSnapshot . set ( targetFileName , previousTargetSnapshot )
237- } else {
238- this . lastSnapshot . delete ( targetFileName )
239- }
240-
241266 const message = `Failed to rename ${ oldFileName } -> ${ newFileName } `
242267 log . error ( message , err )
243268 socket . send (
0 commit comments