Skip to content

Commit e289f2b

Browse files
hackerwinsclaude
andcommitted
Fix Korean IME composition breakage by pausing sync during composition
During Korean IME composition, remote changes applied to the Yorkie Tree via applyChangePack() caused PM-Yorkie state divergence, breaking jaso completion. Switch to SyncMode.RealtimeSyncOff during composition to prevent remote changes from arriving, then resume Realtime sync and flush accumulated changes after compositionend. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9b2a330 commit e289f2b

File tree

5 files changed

+36
-2
lines changed

5 files changed

+36
-2
lines changed

examples/vanilla-prosemirror/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ async function main() {
7474
const view = new EditorView(editorEl, { state });
7575

7676
const binding = new YorkieProseMirrorBinding(view, doc, 'tree', {
77+
client,
7778
cursors: {
7879
enabled: true,
7980
overlayElement: cursorOverlayEl,

packages/prosemirror/examples/basic.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ async function main() {
163163

164164
// 4. Create and initialize binding
165165
const binding = new YorkieProseMirrorBinding(view, doc, 'tree', {
166+
client,
166167
cursors: {
167168
enabled: true,
168169
overlayElement: cursorOverlayEl,

packages/prosemirror/examples/custom-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ async function main() {
210210

211211
// 4. Create and initialize binding
212212
const binding = new YorkieProseMirrorBinding(view, doc, 'tree', {
213+
client,
213214
markMapping,
214215
cursors: {
215216
enabled: true,

packages/prosemirror/src/binding.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { EditorView } from 'prosemirror-view';
22
import type { Transaction } from 'prosemirror-state';
3-
import { Tree } from '@yorkie-js/sdk';
3+
import { Tree, SyncMode } from '@yorkie-js/sdk';
44
import type { MarkMapping, YorkieProseMirrorOptions } from './types';
55
import { buildMarkMapping, invertMapping } from './defaults';
66
import { pmToYorkie } from './convert';
@@ -42,8 +42,10 @@ export class YorkieProseMirrorBinding {
4242
private markMapping: MarkMapping;
4343
private elementToMarkMapping: Record<string, string>;
4444
private wrapperElementName: string;
45+
private client?: { changeSyncMode(doc: any, syncMode: string): Promise<any> };
4546
private isSyncing = false;
4647
private isComposing = false;
48+
private isSyncPaused = false;
4749
private composingBlockRange: { from: number; to: number } | undefined =
4850
undefined;
4951
private hasPendingRemoteChanges = false;
@@ -68,6 +70,7 @@ export class YorkieProseMirrorBinding {
6870
this.elementToMarkMapping = invertMapping(this.markMapping);
6971
this.wrapperElementName = options.wrapperElementName || 'span';
7072
this.onLog = options.onLog;
73+
this.client = options.client;
7174

7275
if (options.cursors?.enabled) {
7376
this.cursorManager = new CursorManager(options.cursors);
@@ -125,6 +128,7 @@ export class YorkieProseMirrorBinding {
125128
* Clean up all subscriptions and overrides.
126129
*/
127130
destroy(): void {
131+
this.resumeRemoteSync();
128132
this.unsubscribeDoc?.();
129133
this.unsubscribePresence?.();
130134
this.cursorManager?.destroy();
@@ -156,6 +160,7 @@ export class YorkieProseMirrorBinding {
156160
private onCompositionStart = (): void => {
157161
this.isComposing = true;
158162
this.composingBlockRange = this.getComposingBlockRange();
163+
this.pauseRemoteSync();
159164
};
160165

161166
private onCompositionEnd = (): void => {
@@ -182,6 +187,25 @@ export class YorkieProseMirrorBinding {
182187
return undefined;
183188
}
184189

190+
private pauseRemoteSync(): void {
191+
if (!this.client || this.isSyncPaused) return;
192+
this.isSyncPaused = true;
193+
this.client
194+
.changeSyncMode(this.doc, SyncMode.RealtimeSyncOff)
195+
.catch((e) => {
196+
this.onLog?.('error', `Failed to pause sync: ${(e as Error).message}`);
197+
this.isSyncPaused = false;
198+
});
199+
}
200+
201+
private resumeRemoteSync(): void {
202+
if (!this.client || !this.isSyncPaused) return;
203+
this.isSyncPaused = false;
204+
this.client.changeSyncMode(this.doc, SyncMode.Realtime).catch((e) => {
205+
this.onLog?.('error', `Failed to resume sync: ${(e as Error).message}`);
206+
});
207+
}
208+
185209
/**
186210
* Check whether a block-level diff overlaps the block being composed.
187211
*/
@@ -209,7 +233,7 @@ export class YorkieProseMirrorBinding {
209233
* Flush all deferred remote changes after composition ends.
210234
*/
211235
private flushPendingRemoteChanges(): void {
212-
if (!this.hasPendingRemoteChanges) return;
236+
if (!this.hasPendingRemoteChanges && !this.isSyncPaused) return;
213237
this.hasPendingRemoteChanges = false;
214238

215239
// Wait for the browser to finish processing the compositionend event
@@ -222,6 +246,9 @@ export class YorkieProseMirrorBinding {
222246
return;
223247
}
224248

249+
// Resume sync first so accumulated remote changes arrive
250+
this.resumeRemoteSync();
251+
225252
// Apply any accumulated remote content changes
226253
try {
227254
this.isSyncing = true;

packages/prosemirror/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,8 @@ export type YorkieProseMirrorOptions = {
6969
cursors?: CursorOptions;
7070
/** Callback for sync log messages. */
7171
onLog?: (type: 'local' | 'remote' | 'error', message: string) => void;
72+
/** Yorkie client instance used to pause/resume sync during IME composition. */
73+
client?: {
74+
changeSyncMode(doc: any, syncMode: string): Promise<any>;
75+
};
7276
};

0 commit comments

Comments
 (0)