Skip to content

Commit 7b1fb73

Browse files
authored
Merge pull request #4786 from udecode/revert/yjs
Revert @platejs/yjs to 52.0.5
2 parents 0a02e15 + a17120e commit 7b1fb73

File tree

2 files changed

+93
-124
lines changed

2 files changed

+93
-124
lines changed

.changeset/silly-wings-thank.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@platejs/yjs": patch
3+
---
4+
5+
revert @platejs/yjs to 52.0.5

packages/yjs/src/lib/BaseYjsPlugin.ts

Lines changed: 88 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)