@@ -87,11 +87,16 @@ async function waitForInitialSync(bindings: YTextBinding[], timeoutMs: number):
8787async function waitForQuiescence ( bindings : YTextBinding [ ] , timeoutMs : number ) : Promise < void > {
8888 const deadline = performance . now ( ) + timeoutMs
8989 let prev = ''
90+ let stableIters = 0
9091 while ( performance . now ( ) < deadline ) {
9192 const snap = bindings . map ( b => b . remoteYtext . toString ( ) + '|' + b . doc . getText ( ) ) . join ( '|' )
92- if ( snap === prev ) return
93- else prev = snap
94- await delay ( 1000 )
93+ if ( snap === prev ) {
94+ if ( stableIters ++ >= 5 ) return
95+ } else {
96+ prev = snap
97+ stableIters = 0
98+ }
99+ await delay ( 100 )
95100 }
96101 assert . fail ( 'timed out waiting for quiescent state' )
97102}
@@ -113,7 +118,7 @@ suite('Collaborative editing', () => {
113118 dispose : ( ) => Promise < void >
114119 }
115120
116- const mkHandles = async ( enableEnsureSync : boolean = true ) : Promise < TestHandles > => {
121+ const mkHandles = async ( ensureSyncTimeoutMs : number = 0 ) : Promise < TestHandles > => {
117122 // In-memory Hocuspocus server on an ephemeral port; no persistence, no signal handlers.
118123 const server = new Server ( { stopOnSignals : false , quiet : true } )
119124 await new Promise < void > ( resolve => server . httpServer . listen ( 0 , '127.0.0.1' , resolve ) )
@@ -125,7 +130,7 @@ suite('Collaborative editing', () => {
125130 vs . workspace . openTextDocument ( { content : '' , language : 'plaintext' } ) ,
126131 vs . workspace . openTextDocument ( { content : '' , language : 'plaintext' } ) ,
127132 ] )
128- const bindings = docs . map ( ( doc , i ) => new YTextBinding ( doc , clients [ i ] , consoleLog , DOC_NAME , enableEnsureSync ) )
133+ const bindings = docs . map ( ( doc , i ) => new YTextBinding ( doc , clients [ i ] , consoleLog , ensureSyncTimeoutMs , DOC_NAME ) )
129134
130135 // Route document changes to the matching binding
131136 // (`YTextBindingManager.onDidChangeTextDocument` does this in production).
@@ -162,34 +167,22 @@ suite('Collaborative editing', () => {
162167 ed0 . selection = new vs . Selection ( 0 , 0 , 0 , 0 )
163168 const text = 'PROBE\n'
164169 await vs . commands . executeCommand ( 'default:type' , { text } )
165- const deadline = performance . now ( ) + 1_000
166- while ( handles . docs [ 1 ] . getText ( ) !== text ) {
167- if ( performance . now ( ) >= deadline ) {
168- assert . strictEqual ( handles . docs [ 1 ] . getText ( ) , text )
169- }
170- await delay ( 50 )
171- }
172-
170+ await waitForQuiescence ( handles . bindings , 1_000 )
171+ assert . strictEqual ( handles . docs [ 1 ] . getText ( ) , text )
173172 await handles . dispose ( )
174173 } )
175174
176- const testConcurrentEdits = async ( handles : TestHandles ) => {
177- await waitForInitialSync ( handles . bindings , 1_000 )
178-
179- // Ensure text editors are visible for both
180- await vs . window . showTextDocument ( handles . docs [ 0 ] , { viewColumn : COLUMNS [ 0 ] , preview : false } )
181- await vs . window . showTextDocument ( handles . docs [ 1 ] , { viewColumn : COLUMNS [ 1 ] , preview : false } )
182-
183- // Drive a batch of edits, alternating between the two documents.
184- // Not waiting between edits opens a local-vs-remote change race window.
175+ /** Drive a batch of edits, alternating between the two documents.
176+ * Not waiting between edits opens a local-vs-remote change race window. */
177+ const makeConcurrentEdits = async ( handles : TestHandles ) => {
185178 const rng = mulberry32 ( 0xc0ffee )
186179 const NUM_EDITS = 100
187180 for ( let i = 0 ; i < NUM_EDITS ; i ++ ) {
188181 await randomEditOn ( handles . docs [ i % 2 ] , COLUMNS [ i % 2 ] , rng )
189182 }
183+ }
190184
191- await waitForQuiescence ( handles . bindings , 3_000 )
192-
185+ const assertEqualStates = ( handles : TestHandles ) => {
193186 const [ d0 , d1 ] = handles . docs . map ( d => d . getText ( ) )
194187 const [ y0 , y1 ] = handles . bindings . map ( b => b . remoteYtext . toString ( ) )
195188
@@ -200,17 +193,29 @@ suite('Collaborative editing', () => {
200193 assert . strictEqual ( d1 , y1 , diffMessage ( 'doc1 vs its Y.Text' , d1 , y1 ) )
201194 }
202195
203- test ( 'Concurrent edits settle on the equal states' , async function ( ) {
196+ test ( 'Concurrent edits settle on equal states' , async function ( ) {
204197 this . timeout ( 20_000 )
205- const handles = await mkHandles ( )
206- await testConcurrentEdits ( handles )
198+ const ensureSyncTimeoutMs = 1_000
199+ const handles = await mkHandles ( ensureSyncTimeoutMs )
200+ await waitForInitialSync ( handles . bindings , 1_000 )
201+ await makeConcurrentEdits ( handles )
202+ // Wait for ensureSync
203+ await delay ( ensureSyncTimeoutMs + 1_000 )
204+ assertEqualStates ( handles )
207205 await handles . dispose ( )
208206 } )
209207
210- test ( 'Concurrent edits settle on the equal states (no resync )' , async function ( ) {
208+ test ( 'Concurrent edits settle on equal states (no ensureSync )' , async function ( ) {
211209 this . timeout ( 20_000 )
212- const handles = await mkHandles ( false )
213- await testConcurrentEdits ( handles )
210+ const handles = await mkHandles ( 0 )
211+ await waitForInitialSync ( handles . bindings , 1_000 )
212+ // Ensure both documents are visible:
213+ // only `TextEditor.edit`s are expected to converge without ensureSync.
214+ await vs . window . showTextDocument ( handles . docs [ 0 ] , { viewColumn : COLUMNS [ 0 ] , preview : false } )
215+ await vs . window . showTextDocument ( handles . docs [ 1 ] , { viewColumn : COLUMNS [ 1 ] , preview : false } )
216+ await makeConcurrentEdits ( handles )
217+ await waitForQuiescence ( handles . bindings , 3_000 )
218+ await assertEqualStates ( handles )
214219 await handles . dispose ( )
215220 } )
216221} )
0 commit comments