@@ -54,7 +54,6 @@ export const BaseYjsPlugin = createTSlatePlugin<YjsConfig>({
5454 if ( ! ydoc ) {
5555 ydoc = new Y . Doc ( ) ;
5656 }
57-
5857 if ( ! awareness ) {
5958 awareness = new Awareness ( ydoc ) ;
6059 }
@@ -126,7 +125,7 @@ export const BaseYjsPlugin = createTSlatePlugin<YjsConfig>({
126125
127126 try {
128127 YjsEditor . disconnect ( editor as any ) ;
129- } catch ( _error ) { }
128+ } catch { }
130129 } ,
131130 /**
132131 * Disconnect from all providers or specific provider types. For WebRTC
@@ -138,7 +137,7 @@ export const BaseYjsPlugin = createTSlatePlugin<YjsConfig>({
138137 * specified, disconnects from all providers.
139138 */
140139 disconnect : ( type ?: YjsProviderType | YjsProviderType [ ] ) => {
141- const { getOptions } = ctx ;
140+ const { editor : _editor , getOptions } = ctx ;
142141 const { _providers } = getOptions ( ) ;
143142
144143 const typesToDisconnect = type
@@ -191,7 +190,6 @@ export const BaseYjsPlugin = createTSlatePlugin<YjsConfig>({
191190
192191 const options = getOptions ( ) ;
193192 const {
194- _providers,
195193 awareness,
196194 providers : providerConfigsOrInstances = [ ] ,
197195 sharedType : customSharedType ,
@@ -205,73 +203,42 @@ export const BaseYjsPlugin = createTSlatePlugin<YjsConfig>({
205203 ) ;
206204 }
207205
208- // Store initial value for potential later use (after sync)
209- let pendingInitialValue : Value | null = null ;
210-
211- // CRITICAL: Check for both null AND undefined to avoid creating default value
212- if ( value != null ) {
213- // != checks for both null and undefined
214- let initialNodes = value as Value ;
215- if ( typeof value === 'string' ) {
216- initialNodes = editor . api . html . deserialize ( {
217- element : value ,
218- } ) as Value ;
219- } else if ( typeof value === 'function' ) {
220- initialNodes = await value ( editor ) ;
221- } else if ( value ) {
222- initialNodes = value ;
223- }
224- if ( ! initialNodes || initialNodes ?. length === 0 ) {
225- initialNodes = editor . api . create . value ( ) ;
226- }
227-
228- // Store for later - will apply after sync if Y.doc is empty
229- pendingInitialValue = initialNodes ;
230- }
231-
232206 // Final providers array that will contain both configured and custom providers
233207 const finalProviders : UnifiedProvider [ ] = [ ] ;
234208
235- // Connect the YjsEditor first to set up slate-yjs bindings.
236- YjsEditor . connect ( editor as any ) ;
237-
238- // CRITICAL: Don't call editor.tf.init yet - wait for first sync to complete
239- // This prevents editor.tf.init from adding an empty paragraph before server content arrives
240- let hasInitialized = false ;
209+ // Track sync state for waiting
210+ let syncResolve : ( ( ) => void ) | null = null ;
211+ const syncPromise = new Promise < void > ( ( resolve ) => {
212+ syncResolve = resolve ;
213+ } ) ;
241214
242- // Then process and create providers
215+ // Create providers FIRST (before connecting YjsEditor)
243216 for ( const item of providerConfigsOrInstances ) {
244217 if ( isProviderConfig ( item ) ) {
245- // It's a configuration object, create the provider
246- const { options, type } = item ;
218+ const { options : providerOptions , type } = item ;
247219
248- if ( ! options ) {
249- console . warn (
250- `[yjs] No options provided for provider type: ${ type } `
251- ) ;
220+ if ( ! providerOptions ) {
252221 continue ;
253222 }
254223
255224 try {
256- // Create provider with shared handlers, Y.Doc, and Awareness
257225 const provider = createProvider ( {
258226 awareness,
259227 doc : ydoc ,
260- options,
228+ options : providerOptions ,
261229 type,
262230 onConnect : ( ) => {
263231 getOptions ( ) . onConnect ?.( { type } ) ;
264- // At least one provider is connected
265232 setOption ( '_isConnected' , true ) ;
266233 } ,
267234 onDisconnect : ( ) => {
268235 getOptions ( ) . onDisconnect ?.( { type } ) ;
269236
270- // Check for any connected providers
271237 const { _providers } = getOptions ( ) ;
272238 const hasConnectedProvider = _providers . some (
273- ( provider ) => provider . isConnected
239+ ( p ) => p . isConnected
274240 ) ;
241+
275242 if ( ! hasConnectedProvider ) {
276243 setOption ( '_isConnected' , false ) ;
277244 }
@@ -283,95 +250,27 @@ export const BaseYjsPlugin = createTSlatePlugin<YjsConfig>({
283250 getOptions ( ) . onSyncChange ?.( { isSynced, type } ) ;
284251 setOption ( '_isSynced' , isSynced ) ;
285252
286- // CRITICAL: Initialize editor AFTER first sync to avoid empty paragraph before server content
287- if ( isSynced && ! hasInitialized ) {
288- hasInitialized = true ;
289-
290- // After first sync completes, apply initial value ONLY if Y.doc is still empty
291- if ( pendingInitialValue ) {
292- const sharedTypeToCheck =
293- customSharedType || ydoc . get ( 'content' , Y . XmlText ) ;
294- const ydocHasContent =
295- sharedTypeToCheck && sharedTypeToCheck . length > 0 ;
296-
297- if ( ydocHasContent ) {
298- pendingInitialValue = null ;
299- } else {
300- // Use custom sharedType if provided, otherwise use default 'content'
301- if ( customSharedType ) {
302- const delta =
303- slateNodesToInsertDelta ( pendingInitialValue ) ;
304- ydoc . transact ( ( ) => {
305- customSharedType . applyDelta ( delta ) ;
306- } ) ;
307- } else {
308- // For default 'content' key, use deterministic state
309- slateToDeterministicYjsState (
310- id ?? editor . id ,
311- pendingInitialValue
312- )
313- . then ( ( initialDelta ) => {
314- ydoc . transact ( ( ) => {
315- Y . applyUpdate ( ydoc , initialDelta ) ;
316- } ) ;
317- } )
318- . catch ( ( ) => {
319- // Ignore errors applying pending value
320- } ) ;
321- }
322-
323- pendingInitialValue = null ;
324- }
325- }
326-
327- // Now call editor.tf.init after Y.doc sync is complete
328- editor . tf . init ( {
329- autoSelect,
330- selection,
331- shouldNormalizeEditor : false ,
332- value : null ,
333- onReady,
334- } ) ;
253+ // Resolve sync promise on first sync
254+ if ( isSynced && syncResolve ) {
255+ syncResolve ( ) ;
256+ syncResolve = null ;
335257 }
336258 } ,
337259 } ) ;
338260 finalProviders . push ( provider ) ;
339- } catch ( error ) {
340- console . warn (
341- `[yjs] Error creating provider of type ${ type } :` ,
342- error
343- ) ;
261+ } catch {
262+ // Provider creation failed
344263 }
345264 } else {
346- // It's a pre-instantiated UnifiedProvider instance
347- const customProvider = item ;
348-
349- // Check if the provider's document matches our shared document
350- if ( customProvider . document !== ydoc ) {
351- console . warn (
352- `[yjs] Custom provider instance (${ customProvider . type } ) has a different Y.Doc. ` +
353- 'This may cause synchronization issues. Ensure custom providers use the shared Y.Doc.'
354- ) ;
355- }
356- // Check if the provider's awareness matches our shared awareness
357- if ( customProvider . awareness !== awareness ) {
358- console . warn (
359- `[yjs] Custom provider instance (${ customProvider . type } ) has a different Awareness instance. ` +
360- 'Ensure custom providers use the shared Awareness instance for cursor consistency.'
361- ) ;
362- }
363-
364- // Add the custom provider to our providers array
365- finalProviders . push ( customProvider ) ;
265+ finalProviders . push ( item ) ;
366266 }
367267 }
368268
369- // Update provider counts after creation
370269 setOption ( '_providers' , finalProviders ) ;
371270
372- // Finally, connect providers if autoConnect is true
373- if ( autoConnect ) {
374- _providers . forEach ( ( provider ) => {
271+ // Connect providers to start sync
272+ if ( autoConnect && finalProviders . length > 0 ) {
273+ finalProviders . forEach ( ( provider ) => {
375274 try {
376275 provider . connect ( ) ;
377276 } catch ( error ) {
@@ -381,6 +280,71 @@ export const BaseYjsPlugin = createTSlatePlugin<YjsConfig>({
381280 } ) ;
382281 }
383282 } ) ;
283+
284+ // Wait for first sync to complete (with timeout)
285+ const SYNC_TIMEOUT = 5000 ;
286+ await Promise . race ( [
287+ syncPromise ,
288+ new Promise < void > ( ( resolve ) => {
289+ setTimeout ( ( ) => {
290+ syncResolve = null ; // Clear to prevent late resolution
291+ resolve ( ) ;
292+ } , SYNC_TIMEOUT ) ;
293+ } ) ,
294+ ] ) ;
384295 }
296+
297+ // After sync, check if ydoc has content from server
298+ // Use custom sharedType if provided, otherwise use default 'content' key
299+ const sharedRoot =
300+ customSharedType ?? ( ydoc . get ( 'content' , Y . XmlText ) as Y . XmlText ) ;
301+
302+ // Only apply initial value if ydoc is empty (no content from server)
303+ if ( sharedRoot . length === 0 && value !== null ) {
304+ let initialNodes = value as Value ;
305+
306+ if ( typeof value === 'string' ) {
307+ initialNodes = editor . api . html . deserialize ( {
308+ element : value ,
309+ } ) as Value ;
310+ } else if ( typeof value === 'function' ) {
311+ initialNodes = await value ( editor ) ;
312+ } else if ( value ) {
313+ initialNodes = value ;
314+ }
315+ if ( ! initialNodes || initialNodes ?. length === 0 ) {
316+ initialNodes = editor . api . create . value ( ) ;
317+ }
318+ if ( customSharedType ) {
319+ const delta = slateNodesToInsertDelta ( initialNodes ) ;
320+ ydoc . transact ( ( ) => {
321+ customSharedType . applyDelta ( delta ) ;
322+ } ) ;
323+ } else {
324+ const initialDelta = await slateToDeterministicYjsState (
325+ id ?? editor . id ,
326+ initialNodes
327+ ) ;
328+ ydoc . transact ( ( ) => {
329+ Y . applyUpdate ( ydoc , initialDelta ) ;
330+ } ) ;
331+ }
332+ }
333+
334+ // NOW connect YjsEditor after sync is complete
335+ YjsEditor . connect ( editor as any ) ;
336+
337+ editor . tf . init ( {
338+ autoSelect,
339+ selection,
340+ shouldNormalizeEditor : false ,
341+ value : null ,
342+ } ) ;
343+
344+ // Force React to re-render by triggering onChange
345+ editor . api . onChange ( ) ;
346+
347+ // Call onReady callback
348+ onReady ?.( { editor, isAsync : true , value : editor . children } ) ;
385349 } ,
386350 } ) ) ;
0 commit comments