@apollohg/react-native-prose-editor 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +12 -7
  2. package/android/build.gradle +7 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +289 -2
  4. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +51 -1
  5. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  6. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +16 -3
  7. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +82 -1
  8. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +403 -45
  9. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +246 -0
  10. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +841 -155
  11. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +125 -8
  12. package/{src/EditorTheme.ts → dist/EditorTheme.d.ts} +12 -52
  13. package/dist/EditorTheme.js +29 -0
  14. package/dist/EditorToolbar.d.ts +129 -0
  15. package/dist/EditorToolbar.js +394 -0
  16. package/dist/NativeEditorBridge.d.ts +242 -0
  17. package/dist/NativeEditorBridge.js +647 -0
  18. package/dist/NativeRichTextEditor.d.ts +142 -0
  19. package/dist/NativeRichTextEditor.js +649 -0
  20. package/dist/YjsCollaboration.d.ts +83 -0
  21. package/dist/YjsCollaboration.js +585 -0
  22. package/dist/addons.d.ts +70 -0
  23. package/dist/addons.js +77 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.js +26 -0
  26. package/dist/schemas.d.ts +35 -0
  27. package/{src/schemas.ts → dist/schemas.js} +62 -27
  28. package/dist/useNativeEditor.d.ts +40 -0
  29. package/dist/useNativeEditor.js +117 -0
  30. package/ios/EditorAddons.swift +26 -3
  31. package/ios/EditorCore.xcframework/Info.plist +5 -5
  32. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  33. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  34. package/ios/EditorLayoutManager.swift +236 -0
  35. package/ios/EditorTheme.swift +51 -1
  36. package/ios/Generated_editor_core.swift +270 -2
  37. package/ios/NativeEditorExpoView.swift +612 -45
  38. package/ios/NativeEditorModule.swift +81 -0
  39. package/ios/PositionBridge.swift +22 -0
  40. package/ios/RenderBridge.swift +427 -39
  41. package/ios/RichTextEditorView.swift +1342 -18
  42. package/ios/editor_coreFFI/editor_coreFFI.h +209 -0
  43. package/package.json +80 -64
  44. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  45. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  46. package/rust/android/x86_64/libeditor_core.so +0 -0
  47. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +404 -4
  48. package/src/EditorToolbar.tsx +0 -620
  49. package/src/NativeEditorBridge.ts +0 -607
  50. package/src/NativeRichTextEditor.tsx +0 -951
  51. package/src/addons.ts +0 -158
  52. package/src/index.ts +0 -63
  53. package/src/useNativeEditor.ts +0 -173
@@ -0,0 +1,83 @@
1
+ import { type CollaborationPeer, type DocumentJSON, type EncodedCollaborationStateInput, type Selection } from './NativeEditorBridge';
2
+ import type { RemoteSelectionDecoration } from './NativeRichTextEditor';
3
+ export type YjsTransportStatus = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
4
+ export interface YjsRetryContext {
5
+ attempt: number;
6
+ documentId: string;
7
+ lastError?: Error;
8
+ }
9
+ export type YjsRetryInterval = number | ((context: YjsRetryContext) => number | null | false);
10
+ export interface LocalAwarenessUser {
11
+ userId: string;
12
+ name: string;
13
+ color: string;
14
+ avatarUrl?: string;
15
+ extra?: Record<string, unknown>;
16
+ }
17
+ export interface LocalAwarenessState {
18
+ user: LocalAwarenessUser;
19
+ selection?: {
20
+ anchor: number;
21
+ head: number;
22
+ };
23
+ focused?: boolean;
24
+ }
25
+ export interface YjsCollaborationState {
26
+ documentId: string;
27
+ status: YjsTransportStatus;
28
+ isConnected: boolean;
29
+ documentJson: DocumentJSON;
30
+ lastError?: Error;
31
+ }
32
+ export interface YjsCollaborationOptions {
33
+ documentId: string;
34
+ createWebSocket: () => WebSocket;
35
+ connect?: boolean;
36
+ retryIntervalMs?: YjsRetryInterval | false;
37
+ fragmentName?: string;
38
+ initialDocumentJson?: DocumentJSON;
39
+ initialEncodedState?: EncodedCollaborationStateInput;
40
+ localAwareness: LocalAwarenessUser;
41
+ onPeersChange?: (peers: CollaborationPeer[]) => void;
42
+ onStateChange?: (state: YjsCollaborationState) => void;
43
+ onError?: (error: Error) => void;
44
+ }
45
+ export interface YjsCollaborationController {
46
+ readonly state: YjsCollaborationState;
47
+ readonly peers: CollaborationPeer[];
48
+ connect(): void;
49
+ disconnect(): void;
50
+ reconnect(): void;
51
+ destroy(): void;
52
+ getEncodedState(): Uint8Array;
53
+ getEncodedStateBase64(): string;
54
+ applyEncodedState(encodedState: EncodedCollaborationStateInput): void;
55
+ replaceEncodedState(encodedState: EncodedCollaborationStateInput): void;
56
+ updateLocalAwareness(partial: Partial<LocalAwarenessState>): void;
57
+ handleLocalDocumentChange(doc: DocumentJSON): void;
58
+ handleSelectionChange(selection: Selection): void;
59
+ handleFocusChange(focused: boolean): void;
60
+ }
61
+ export interface UseYjsCollaborationResult {
62
+ state: YjsCollaborationState;
63
+ peers: CollaborationPeer[];
64
+ isConnected: boolean;
65
+ connect(): void;
66
+ disconnect(): void;
67
+ reconnect(): void;
68
+ getEncodedState(): Uint8Array;
69
+ getEncodedStateBase64(): string;
70
+ applyEncodedState(encodedState: EncodedCollaborationStateInput): void;
71
+ replaceEncodedState(encodedState: EncodedCollaborationStateInput): void;
72
+ updateLocalAwareness(partial: Partial<LocalAwarenessState>): void;
73
+ editorBindings: {
74
+ valueJSON: DocumentJSON;
75
+ remoteSelections: RemoteSelectionDecoration[];
76
+ onContentChangeJSON: (doc: DocumentJSON) => void;
77
+ onSelectionChange: (selection: Selection) => void;
78
+ onFocus: () => void;
79
+ onBlur: () => void;
80
+ };
81
+ }
82
+ export declare function createYjsCollaborationController(options: YjsCollaborationOptions): YjsCollaborationController;
83
+ export declare function useYjsCollaboration(options: YjsCollaborationOptions): UseYjsCollaborationResult;
@@ -0,0 +1,585 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createYjsCollaborationController = createYjsCollaborationController;
4
+ exports.useYjsCollaboration = useYjsCollaboration;
5
+ const react_1 = require("react");
6
+ const NativeEditorBridge_1 = require("./NativeEditorBridge");
7
+ const DEFAULT_RETRY_BASE_INTERVAL_MS = 500;
8
+ const DEFAULT_RETRY_MAX_INTERVAL_MS = 30000;
9
+ const Y_WEBSOCKET_MESSAGE_QUERY_AWARENESS = 3;
10
+ const DEFAULT_YJS_FRAGMENT_NAME = 'default';
11
+ const EMPTY_DOCUMENT = {
12
+ type: 'doc',
13
+ content: [
14
+ {
15
+ type: 'paragraph',
16
+ },
17
+ ],
18
+ };
19
+ const SELECTION_AWARENESS_DEBOUNCE_MS = 40;
20
+ function cloneDocument(doc) {
21
+ return JSON.parse(JSON.stringify(doc ?? EMPTY_DOCUMENT));
22
+ }
23
+ function awarenessToRecord(awareness) {
24
+ return JSON.parse(JSON.stringify(awareness));
25
+ }
26
+ function normalizeMessageBytes(data) {
27
+ if (data instanceof ArrayBuffer) {
28
+ return Array.from(new Uint8Array(data));
29
+ }
30
+ if (ArrayBuffer.isView(data)) {
31
+ return Array.from(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
32
+ }
33
+ if (typeof data === 'string') {
34
+ try {
35
+ const parsed = JSON.parse(data);
36
+ return Array.isArray(parsed) ? parsed : null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ function sendBinaryMessages(socket, messages) {
45
+ if (!socket || socket.readyState !== WebSocket.OPEN)
46
+ return;
47
+ for (const message of messages) {
48
+ socket.send(Uint8Array.from(message).buffer);
49
+ }
50
+ }
51
+ function readFirstVarUint(bytes) {
52
+ let value = 0;
53
+ let shift = 0;
54
+ for (let index = 0; index < bytes.length && index < 5; index += 1) {
55
+ const byte = bytes[index];
56
+ value |= (byte & 0x7f) << shift;
57
+ if ((byte & 0x80) === 0) {
58
+ return value;
59
+ }
60
+ shift += 7;
61
+ }
62
+ return null;
63
+ }
64
+ function selectionToAwarenessRange(selection) {
65
+ if (selection.type !== 'text')
66
+ return undefined;
67
+ return {
68
+ anchor: selection.anchor ?? 0,
69
+ head: selection.head ?? selection.anchor ?? 0,
70
+ };
71
+ }
72
+ function peersToRemoteSelections(peers) {
73
+ return peers.flatMap((peer) => {
74
+ if (peer.isLocal || !peer.state || typeof peer.state !== 'object') {
75
+ return [];
76
+ }
77
+ const state = peer.state;
78
+ const selection = state.selection;
79
+ if (!selection || typeof selection !== 'object') {
80
+ return [];
81
+ }
82
+ const anchor = Number(selection.anchor);
83
+ const head = Number(selection.head);
84
+ if (!Number.isFinite(anchor) || !Number.isFinite(head)) {
85
+ return [];
86
+ }
87
+ const user = state.user && typeof state.user === 'object'
88
+ ? state.user
89
+ : null;
90
+ return [
91
+ {
92
+ clientId: peer.clientId,
93
+ anchor,
94
+ head,
95
+ color: typeof user?.color === 'string' && user.color.length > 0
96
+ ? user.color
97
+ : '#007AFF',
98
+ name: typeof user?.name === 'string' && user.name.length > 0 ? user.name : undefined,
99
+ avatarUrl: typeof user?.avatarUrl === 'string' && user.avatarUrl.length > 0
100
+ ? user.avatarUrl
101
+ : undefined,
102
+ isFocused: state.focused !== false,
103
+ },
104
+ ];
105
+ });
106
+ }
107
+ function encodeInitialStateKey(encodedState) {
108
+ if (encodedState == null)
109
+ return '';
110
+ return (0, NativeEditorBridge_1.encodeCollaborationStateBase64)(encodedState);
111
+ }
112
+ function encodeDocumentKey(doc) {
113
+ if (doc == null)
114
+ return '';
115
+ return JSON.stringify(doc);
116
+ }
117
+ class YjsCollaborationControllerImpl {
118
+ constructor(options, callbacks = {}) {
119
+ this.socket = null;
120
+ this.destroyed = false;
121
+ this.retryAttempt = 0;
122
+ this.retryTimer = null;
123
+ this.isManuallyDisconnected = false;
124
+ this.pendingAwarenessTimer = null;
125
+ this._peers = [];
126
+ this.callbacks = callbacks;
127
+ this.createWebSocket = options.createWebSocket;
128
+ this.retryIntervalMs = options.retryIntervalMs;
129
+ const hasInitialEncodedState = options.initialEncodedState != null;
130
+ this.localAwarenessState = {
131
+ user: options.localAwareness,
132
+ focused: false,
133
+ };
134
+ this.bridge = NativeEditorBridge_1.NativeCollaborationBridge.create({
135
+ fragmentName: options.fragmentName ?? DEFAULT_YJS_FRAGMENT_NAME,
136
+ initialEncodedState: options.initialEncodedState,
137
+ localAwareness: awarenessToRecord(this.localAwarenessState),
138
+ });
139
+ this._state = {
140
+ documentId: options.documentId,
141
+ status: 'idle',
142
+ isConnected: false,
143
+ documentJson: hasInitialEncodedState
144
+ ? this.bridge.getDocumentJson()
145
+ : cloneDocument(options.initialDocumentJson),
146
+ };
147
+ this._peers = this.bridge.getPeers();
148
+ if (options.connect !== false) {
149
+ this.connect();
150
+ }
151
+ }
152
+ get state() {
153
+ return this._state;
154
+ }
155
+ get peers() {
156
+ return this._peers;
157
+ }
158
+ connect() {
159
+ if (this.destroyed)
160
+ return;
161
+ this.isManuallyDisconnected = false;
162
+ this.cancelRetry();
163
+ if (this.socket &&
164
+ (this.socket.readyState === WebSocket.OPEN ||
165
+ this.socket.readyState === WebSocket.CONNECTING)) {
166
+ return;
167
+ }
168
+ this.setState({
169
+ status: 'connecting',
170
+ isConnected: false,
171
+ lastError: undefined,
172
+ });
173
+ let socket;
174
+ try {
175
+ socket = this.createWebSocket();
176
+ }
177
+ catch (cause) {
178
+ const error = cause instanceof Error
179
+ ? cause
180
+ : new Error('Yjs collaboration transport initialization failed');
181
+ this.setState({
182
+ status: 'error',
183
+ isConnected: false,
184
+ lastError: error,
185
+ });
186
+ this.callbacks.onError?.(error);
187
+ this.scheduleRetry(error);
188
+ return;
189
+ }
190
+ this.socket = socket;
191
+ const binarySocket = socket;
192
+ try {
193
+ binarySocket.binaryType = 'arraybuffer';
194
+ }
195
+ catch {
196
+ // React Native WebSocket implementations may ignore this.
197
+ }
198
+ socket.onopen = () => {
199
+ if (this.destroyed || this.socket !== socket)
200
+ return;
201
+ this.retryAttempt = 0;
202
+ this.cancelRetry();
203
+ this.setState({
204
+ status: 'connected',
205
+ isConnected: true,
206
+ lastError: undefined,
207
+ });
208
+ const result = this.bridge.start();
209
+ this.applyResult(result);
210
+ sendBinaryMessages(socket, result.messages);
211
+ };
212
+ socket.onmessage = (event) => {
213
+ if (this.destroyed || this.socket !== socket)
214
+ return;
215
+ const bytes = normalizeMessageBytes(event.data);
216
+ if (!bytes)
217
+ return;
218
+ if (readFirstVarUint(bytes) === Y_WEBSOCKET_MESSAGE_QUERY_AWARENESS) {
219
+ try {
220
+ this.commitLocalAwareness();
221
+ }
222
+ catch (error) {
223
+ this.handleTransportFailure(error instanceof Error
224
+ ? error
225
+ : new Error('Yjs collaboration protocol error'), socket);
226
+ }
227
+ return;
228
+ }
229
+ try {
230
+ const result = this.bridge.handleMessage(bytes);
231
+ this.applyResult(result);
232
+ sendBinaryMessages(socket, result.messages);
233
+ }
234
+ catch (error) {
235
+ this.handleTransportFailure(error instanceof Error ? error : new Error('Yjs collaboration protocol error'), socket);
236
+ }
237
+ };
238
+ socket.onerror = () => {
239
+ if (this.destroyed || this.socket !== socket)
240
+ return;
241
+ this.handleTransportFailure(new Error('Yjs collaboration transport error'), socket, false);
242
+ };
243
+ socket.onclose = () => {
244
+ if (this.destroyed || this.socket !== socket)
245
+ return;
246
+ this.socket = null;
247
+ this.clearPeers();
248
+ this.setState({
249
+ status: this._state.status === 'error' ? 'error' : 'disconnected',
250
+ isConnected: false,
251
+ });
252
+ this.scheduleRetry(this._state.lastError);
253
+ };
254
+ }
255
+ disconnect() {
256
+ if (this.destroyed)
257
+ return;
258
+ this.isManuallyDisconnected = true;
259
+ this.retryAttempt = 0;
260
+ this.cancelRetry();
261
+ const socket = this.socket;
262
+ this.socket = null;
263
+ if (socket?.readyState === WebSocket.OPEN) {
264
+ const result = this.bridge.clearLocalAwareness();
265
+ this.applyResult(result);
266
+ sendBinaryMessages(socket, result.messages);
267
+ socket.close();
268
+ }
269
+ if (socket &&
270
+ socket.readyState !== WebSocket.CLOSED &&
271
+ socket.readyState !== WebSocket.CLOSING) {
272
+ try {
273
+ socket.close();
274
+ }
275
+ catch {
276
+ // Ignore close failures while disconnecting locally.
277
+ }
278
+ }
279
+ this.clearPeers();
280
+ this.setState({
281
+ status: 'disconnected',
282
+ isConnected: false,
283
+ lastError: undefined,
284
+ });
285
+ }
286
+ reconnect() {
287
+ this.disconnect();
288
+ this.connect();
289
+ }
290
+ getEncodedState() {
291
+ if (this.destroyed)
292
+ return new Uint8Array();
293
+ return this.bridge.getEncodedState();
294
+ }
295
+ getEncodedStateBase64() {
296
+ if (this.destroyed)
297
+ return '';
298
+ return this.bridge.getEncodedStateBase64();
299
+ }
300
+ applyEncodedState(encodedState) {
301
+ if (this.destroyed)
302
+ return;
303
+ const result = this.bridge.applyEncodedState(encodedState);
304
+ this.applyResult(result);
305
+ sendBinaryMessages(this.socket, result.messages);
306
+ }
307
+ replaceEncodedState(encodedState) {
308
+ if (this.destroyed)
309
+ return;
310
+ const result = this.bridge.replaceEncodedState(encodedState);
311
+ this.applyResult(result);
312
+ sendBinaryMessages(this.socket, result.messages);
313
+ }
314
+ destroy() {
315
+ if (this.destroyed)
316
+ return;
317
+ this.destroyed = true;
318
+ this.cancelRetry();
319
+ this.cancelPendingAwarenessSync();
320
+ this.disconnect();
321
+ this.bridge.destroy();
322
+ }
323
+ updateLocalAwareness(partial) {
324
+ if (this.destroyed)
325
+ return;
326
+ this.localAwarenessState = this.mergeLocalAwareness(partial);
327
+ this.commitLocalAwareness();
328
+ }
329
+ handleLocalDocumentChange(doc) {
330
+ if (this.destroyed)
331
+ return;
332
+ const result = this.bridge.applyLocalDocumentJson(doc);
333
+ this.applyResult(result);
334
+ sendBinaryMessages(this.socket, result.messages);
335
+ }
336
+ handleSelectionChange(selection) {
337
+ if (this.destroyed)
338
+ return;
339
+ this.localAwarenessState = this.mergeLocalAwareness({
340
+ focused: true,
341
+ selection: selectionToAwarenessRange(selection),
342
+ });
343
+ this.scheduleAwarenessSync();
344
+ }
345
+ handleFocusChange(focused) {
346
+ if (this.destroyed)
347
+ return;
348
+ this.localAwarenessState = this.mergeLocalAwareness({ focused });
349
+ this.commitLocalAwareness();
350
+ }
351
+ applyResult(result) {
352
+ if (result.documentChanged && result.documentJson) {
353
+ this.setState({
354
+ documentJson: cloneDocument(result.documentJson),
355
+ });
356
+ }
357
+ if (result.peersChanged && result.peers) {
358
+ this._peers = result.peers;
359
+ this.callbacks.onPeersChange?.(this._peers);
360
+ }
361
+ }
362
+ clearPeers() {
363
+ if (this._peers.length === 0)
364
+ return;
365
+ this._peers = [];
366
+ this.callbacks.onPeersChange?.(this._peers);
367
+ }
368
+ setState(patch) {
369
+ this._state = {
370
+ ...this._state,
371
+ ...patch,
372
+ };
373
+ this.callbacks.onStateChange?.(this._state);
374
+ }
375
+ mergeLocalAwareness(partial) {
376
+ return {
377
+ ...this.localAwarenessState,
378
+ ...partial,
379
+ user: {
380
+ ...this.localAwarenessState.user,
381
+ ...(partial.user ?? {}),
382
+ },
383
+ };
384
+ }
385
+ scheduleAwarenessSync() {
386
+ this.cancelPendingAwarenessSync();
387
+ this.pendingAwarenessTimer = setTimeout(() => {
388
+ this.pendingAwarenessTimer = null;
389
+ this.commitLocalAwareness();
390
+ }, SELECTION_AWARENESS_DEBOUNCE_MS);
391
+ }
392
+ cancelPendingAwarenessSync() {
393
+ if (this.pendingAwarenessTimer == null)
394
+ return;
395
+ clearTimeout(this.pendingAwarenessTimer);
396
+ this.pendingAwarenessTimer = null;
397
+ }
398
+ commitLocalAwareness() {
399
+ this.cancelPendingAwarenessSync();
400
+ const result = this.bridge.setLocalAwareness(awarenessToRecord(this.localAwarenessState));
401
+ this.applyResult(result);
402
+ sendBinaryMessages(this.socket, result.messages);
403
+ }
404
+ handleTransportFailure(error, socket, closeSocket = true) {
405
+ if (this.destroyed || this.socket !== socket)
406
+ return;
407
+ this.socket = null;
408
+ if (closeSocket && socket.readyState !== WebSocket.CLOSED) {
409
+ try {
410
+ socket.close();
411
+ }
412
+ catch {
413
+ // Ignore close failures while reporting the original transport error.
414
+ }
415
+ }
416
+ this.clearPeers();
417
+ this.setState({
418
+ status: 'error',
419
+ isConnected: false,
420
+ lastError: error,
421
+ });
422
+ this.callbacks.onError?.(error);
423
+ this.scheduleRetry(error);
424
+ }
425
+ scheduleRetry(lastError) {
426
+ if (this.destroyed || this.isManuallyDisconnected)
427
+ return;
428
+ const delayMs = this.resolveRetryDelay(lastError);
429
+ if (delayMs == null)
430
+ return;
431
+ this.cancelRetry();
432
+ this.retryAttempt += 1;
433
+ this.retryTimer = setTimeout(() => {
434
+ this.retryTimer = null;
435
+ if (this.destroyed || this.isManuallyDisconnected)
436
+ return;
437
+ this.connect();
438
+ }, delayMs);
439
+ }
440
+ resolveRetryDelay(lastError) {
441
+ if (this.retryIntervalMs === false)
442
+ return null;
443
+ const attempt = this.retryAttempt + 1;
444
+ const value = this.retryIntervalMs == null
445
+ ? defaultRetryIntervalMs(attempt)
446
+ : typeof this.retryIntervalMs === 'function'
447
+ ? this.retryIntervalMs({
448
+ attempt,
449
+ documentId: this._state.documentId,
450
+ lastError,
451
+ })
452
+ : this.retryIntervalMs;
453
+ if (value === false || value == null) {
454
+ return null;
455
+ }
456
+ if (!Number.isFinite(value) || value < 0) {
457
+ return null;
458
+ }
459
+ return value;
460
+ }
461
+ cancelRetry() {
462
+ if (this.retryTimer == null)
463
+ return;
464
+ clearTimeout(this.retryTimer);
465
+ this.retryTimer = null;
466
+ }
467
+ }
468
+ function defaultRetryIntervalMs(attempt) {
469
+ return Math.min(DEFAULT_RETRY_BASE_INTERVAL_MS * 2 ** Math.max(0, attempt - 1), DEFAULT_RETRY_MAX_INTERVAL_MS);
470
+ }
471
+ function createYjsCollaborationController(options) {
472
+ return new YjsCollaborationControllerImpl(options, {
473
+ onStateChange: options.onStateChange,
474
+ onPeersChange: options.onPeersChange,
475
+ onError: options.onError,
476
+ });
477
+ }
478
+ function useYjsCollaboration(options) {
479
+ const callbacksRef = (0, react_1.useRef)({
480
+ onPeersChange: options.onPeersChange,
481
+ onStateChange: options.onStateChange,
482
+ onError: options.onError,
483
+ });
484
+ callbacksRef.current = {
485
+ onPeersChange: options.onPeersChange,
486
+ onStateChange: options.onStateChange,
487
+ onError: options.onError,
488
+ };
489
+ const createWebSocketRef = (0, react_1.useRef)(options.createWebSocket);
490
+ createWebSocketRef.current = options.createWebSocket;
491
+ const controllerRef = (0, react_1.useRef)(null);
492
+ const initialDocumentKey = encodeDocumentKey(options.initialDocumentJson);
493
+ const initialEncodedStateKey = encodeInitialStateKey(options.initialEncodedState);
494
+ const localAwarenessKey = JSON.stringify(options.localAwareness);
495
+ const [state, setState] = (0, react_1.useState)({
496
+ documentId: options.documentId,
497
+ status: 'idle',
498
+ isConnected: false,
499
+ documentJson: cloneDocument(options.initialDocumentJson),
500
+ });
501
+ const [peers, setPeers] = (0, react_1.useState)([]);
502
+ (0, react_1.useEffect)(() => {
503
+ try {
504
+ const controller = new YjsCollaborationControllerImpl({
505
+ ...options,
506
+ createWebSocket: () => createWebSocketRef.current(),
507
+ }, {
508
+ onStateChange: (nextState) => {
509
+ setState({ ...nextState });
510
+ callbacksRef.current.onStateChange?.(nextState);
511
+ },
512
+ onPeersChange: (nextPeers) => {
513
+ setPeers([...nextPeers]);
514
+ callbacksRef.current.onPeersChange?.(nextPeers);
515
+ },
516
+ onError: (error) => {
517
+ callbacksRef.current.onError?.(error);
518
+ },
519
+ });
520
+ controllerRef.current = controller;
521
+ setState({ ...controller.state });
522
+ setPeers([...controller.peers]);
523
+ }
524
+ catch (error) {
525
+ const nextError = error instanceof Error
526
+ ? error
527
+ : new Error('Yjs collaboration initialization failed');
528
+ const nextState = {
529
+ documentId: options.documentId,
530
+ status: 'error',
531
+ isConnected: false,
532
+ documentJson: cloneDocument(options.initialDocumentJson),
533
+ lastError: nextError,
534
+ };
535
+ controllerRef.current = null;
536
+ setState(nextState);
537
+ setPeers([]);
538
+ callbacksRef.current.onStateChange?.(nextState);
539
+ callbacksRef.current.onError?.(nextError);
540
+ }
541
+ return () => {
542
+ controllerRef.current?.destroy();
543
+ controllerRef.current = null;
544
+ };
545
+ // eslint-disable-next-line react-hooks/exhaustive-deps
546
+ }, [options.documentId, options.fragmentName, initialDocumentKey, initialEncodedStateKey]);
547
+ (0, react_1.useEffect)(() => {
548
+ controllerRef.current?.updateLocalAwareness({
549
+ user: options.localAwareness,
550
+ });
551
+ }, [localAwarenessKey, options.localAwareness]);
552
+ (0, react_1.useEffect)(() => {
553
+ const controller = controllerRef.current;
554
+ if (!controller)
555
+ return;
556
+ if (options.connect === false) {
557
+ controller.disconnect();
558
+ }
559
+ else {
560
+ controller.connect();
561
+ }
562
+ }, [options.connect, options.documentId]);
563
+ return {
564
+ state,
565
+ peers,
566
+ isConnected: state.isConnected,
567
+ connect: () => controllerRef.current?.connect(),
568
+ disconnect: () => controllerRef.current?.disconnect(),
569
+ reconnect: () => controllerRef.current?.reconnect(),
570
+ getEncodedState: () => controllerRef.current?.getEncodedState() ?? new Uint8Array(),
571
+ getEncodedStateBase64: () => controllerRef.current?.getEncodedStateBase64() ??
572
+ (0, NativeEditorBridge_1.encodeCollaborationStateBase64)(new Uint8Array()),
573
+ applyEncodedState: (encodedState) => controllerRef.current?.applyEncodedState(encodedState),
574
+ replaceEncodedState: (encodedState) => controllerRef.current?.replaceEncodedState(encodedState),
575
+ updateLocalAwareness: (partial) => controllerRef.current?.updateLocalAwareness(partial),
576
+ editorBindings: {
577
+ valueJSON: state.documentJson,
578
+ remoteSelections: peersToRemoteSelections(peers),
579
+ onContentChangeJSON: (doc) => controllerRef.current?.handleLocalDocumentChange(doc),
580
+ onSelectionChange: (selection) => controllerRef.current?.handleSelectionChange(selection),
581
+ onFocus: () => controllerRef.current?.handleFocusChange(true),
582
+ onBlur: () => controllerRef.current?.handleFocusChange(false),
583
+ },
584
+ };
585
+ }