@veltdev/draftjs-crdt 1.0.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.
package/esm/index.js ADDED
@@ -0,0 +1,1815 @@
1
+ import { Store } from '@veltdev/crdt';
2
+ import * as Y from 'yjs';
3
+ import { convertToRaw, convertFromRaw, EditorState, ContentState } from 'draft-js';
4
+ import { useState, useRef, useEffect, useCallback } from 'react';
5
+ import { useVeltClient, useVeltInitState } from '@veltdev/react';
6
+
7
+ // POLICY: Every function/method in this file MUST be wrapped in try/catch.
8
+ // The package must NEVER throw an exception to the consumer's code.
9
+ const LOG_PREFIX = 'DraftjsCRDT';
10
+ /**
11
+ * Log an error/warning — only visible when debug mode is enabled
12
+ * via sessionStorage.setItem('forceCrdtDebugMode', 'true').
13
+ */
14
+ function logError(message, error) {
15
+ try {
16
+ if (typeof sessionStorage !== 'undefined' &&
17
+ sessionStorage.getItem('forceCrdtDebugMode')) {
18
+ console.warn(`${LOG_PREFIX} ${message}`, error !== undefined ? error : '');
19
+ }
20
+ }
21
+ catch {
22
+ // sessionStorage may not be available (SSR / iframe restrictions)
23
+ }
24
+ }
25
+
26
+ // POLICY: Public package helpers must never throw into consumer code.
27
+ const CONFIG_MAP_KEY$1 = 'veltConfig';
28
+ const INITIALIZED_FLAG = '__docInitialized';
29
+ const LEGACY_FLAG = 'hasContentSet';
30
+ const META_NODE = 'draftjs-meta';
31
+ const BLOCK_NODE = 'draftjs-block';
32
+ const RAW_ATTR = 'raw';
33
+ const SCHEMA_ATTR = 'schema';
34
+ const SCHEMA_VERSION = 'draftjs-raw-v1';
35
+ function applyInitialContent(doc, xmlFragment, rawContent, force, origin) {
36
+ try {
37
+ const configMap = doc.getMap(CONFIG_MAP_KEY$1);
38
+ if (!force && (configMap.get(INITIALIZED_FLAG) || configMap.get(LEGACY_FLAG))) {
39
+ return false;
40
+ }
41
+ if (!force && xmlFragment.length > 0) {
42
+ return false;
43
+ }
44
+ doc.transact(() => {
45
+ writeRawToXmlFragment(xmlFragment, rawContent);
46
+ configMap.set(INITIALIZED_FLAG, true);
47
+ }, origin);
48
+ return true;
49
+ }
50
+ catch (error) {
51
+ logError('Failed to apply initial content', error);
52
+ return false;
53
+ }
54
+ }
55
+ function normalizeInitialContent(initialContent) {
56
+ try {
57
+ if (initialContent === undefined || initialContent === null) {
58
+ return null;
59
+ }
60
+ if (typeof initialContent === 'string') {
61
+ return convertToRaw(ContentState.createFromText(initialContent));
62
+ }
63
+ if (isRawDraftContentState(initialContent)) {
64
+ return initialContent;
65
+ }
66
+ return null;
67
+ }
68
+ catch (error) {
69
+ logError('Failed to normalize initial content', error);
70
+ return null;
71
+ }
72
+ }
73
+ function rawFromEditorState(editorState) {
74
+ try {
75
+ return convertToRaw(editorState.getCurrentContent());
76
+ }
77
+ catch (error) {
78
+ logError('Failed to read Draft.js editor state', error);
79
+ return null;
80
+ }
81
+ }
82
+ function editorStateFromRaw(rawContent, previousState) {
83
+ try {
84
+ const contentState = convertFromRaw(rawContent);
85
+ if (!previousState) {
86
+ return EditorState.createWithContent(contentState);
87
+ }
88
+ let nextState = EditorState.push(previousState, contentState, 'insert-fragment');
89
+ const previousSelection = previousState.getSelection();
90
+ if (previousSelection.getHasFocus() &&
91
+ contentState.getBlockMap().has(previousSelection.getAnchorKey()) &&
92
+ contentState.getBlockMap().has(previousSelection.getFocusKey())) {
93
+ nextState = EditorState.forceSelection(nextState, previousSelection);
94
+ }
95
+ return nextState;
96
+ }
97
+ catch (error) {
98
+ logError('Failed to create Draft.js editor state from CRDT content', error);
99
+ return null;
100
+ }
101
+ }
102
+ function writeRawToXmlFragment(xmlFragment, rawContent) {
103
+ try {
104
+ if (xmlFragment.length > 0) {
105
+ xmlFragment.delete(0, xmlFragment.length);
106
+ }
107
+ xmlFragment.insert(0, buildXmlNodes(rawContent));
108
+ }
109
+ catch (error) {
110
+ logError('Failed to write Draft.js content to XML fragment', error);
111
+ }
112
+ }
113
+ function rawFromXmlFragment(xmlFragment) {
114
+ try {
115
+ const nodes = xmlFragment.toArray();
116
+ const meta = nodes.find((node) => isXmlElement(node, META_NODE));
117
+ const rawJson = meta instanceof Y.XmlElement ? meta.getAttribute(RAW_ATTR) : undefined;
118
+ let rawFromMeta = null;
119
+ if (typeof rawJson === 'string') {
120
+ const parsed = JSON.parse(rawJson);
121
+ if (isRawDraftContentState(parsed)) {
122
+ rawFromMeta = parsed;
123
+ }
124
+ }
125
+ const blocks = nodes
126
+ .filter((node) => isXmlElement(node, BLOCK_NODE))
127
+ .map(readBlockFromXml)
128
+ .filter((block) => !!block);
129
+ const genericBlocks = nodes
130
+ .filter((node) => !isXmlElement(node, META_NODE) && !isXmlElement(node, BLOCK_NODE))
131
+ .map((node, index) => rawBlockFromText(readTextFromXmlNode(node).trim(), index))
132
+ .filter((block) => !!block);
133
+ if (rawFromMeta) {
134
+ if (genericBlocks.length === 0)
135
+ return rawFromMeta;
136
+ return {
137
+ blocks: [...rawFromMeta.blocks, ...genericBlocks],
138
+ entityMap: rawFromMeta.entityMap ?? {},
139
+ };
140
+ }
141
+ if (blocks.length > 0) {
142
+ return { blocks: [...blocks, ...genericBlocks], entityMap: {} };
143
+ }
144
+ const genericText = nodes.map(readTextFromXmlNode).join('\n').trim();
145
+ if (genericText) {
146
+ return {
147
+ blocks: [rawBlockFromText(genericText, 0)],
148
+ entityMap: {},
149
+ };
150
+ }
151
+ return null;
152
+ }
153
+ catch (error) {
154
+ logError('Failed to read Draft.js content from XML fragment', error);
155
+ return null;
156
+ }
157
+ }
158
+ function rawFromXmlLikeString(value) {
159
+ try {
160
+ if (!value) {
161
+ return null;
162
+ }
163
+ const rawFromMeta = rawFromDraftMetaString(value);
164
+ if (rawFromMeta)
165
+ return rawFromMeta;
166
+ const text = stripXmlTags(value)
167
+ .replace(/\s+/g, ' ')
168
+ .trim();
169
+ if (!text)
170
+ return null;
171
+ return {
172
+ blocks: [rawBlockFromText(text, 0)],
173
+ entityMap: {},
174
+ };
175
+ }
176
+ catch (error) {
177
+ logError('Failed to read Draft.js content from XML-like string', error);
178
+ return null;
179
+ }
180
+ }
181
+ function rawContentsEqual(a, b) {
182
+ try {
183
+ if (!a || !b)
184
+ return false;
185
+ return serializeRawContent(a) === serializeRawContent(b);
186
+ }
187
+ catch {
188
+ return false;
189
+ }
190
+ }
191
+ function serializeRawContent(rawContent) {
192
+ try {
193
+ return JSON.stringify(rawContent);
194
+ }
195
+ catch (error) {
196
+ logError('Failed to serialize Draft.js raw content', error);
197
+ return '';
198
+ }
199
+ }
200
+ function isRawDraftContentState(value) {
201
+ try {
202
+ if (!value || typeof value !== 'object')
203
+ return false;
204
+ const candidate = value;
205
+ return Array.isArray(candidate.blocks) && !!candidate.entityMap;
206
+ }
207
+ catch {
208
+ return false;
209
+ }
210
+ }
211
+ function buildXmlNodes(rawContent) {
212
+ const meta = new Y.XmlElement(META_NODE);
213
+ meta.setAttribute(SCHEMA_ATTR, SCHEMA_VERSION);
214
+ meta.setAttribute(RAW_ATTR, serializeRawContent(rawContent));
215
+ const blocks = rawContent.blocks.map((block) => {
216
+ const blockElement = new Y.XmlElement(BLOCK_NODE);
217
+ blockElement.setAttribute('key', block.key);
218
+ blockElement.setAttribute('type', block.type);
219
+ blockElement.setAttribute('depth', String(block.depth ?? 0));
220
+ blockElement.setAttribute('data', JSON.stringify(block.data ?? {}));
221
+ blockElement.setAttribute('entityRanges', JSON.stringify(block.entityRanges ?? []));
222
+ const text = new Y.XmlText();
223
+ insertStyledText(text, block);
224
+ blockElement.insert(0, [text]);
225
+ return blockElement;
226
+ });
227
+ return [meta, ...blocks];
228
+ }
229
+ function insertStyledText(xmlText, block) {
230
+ const text = block.text ?? '';
231
+ let index = 0;
232
+ let yIndex = 0;
233
+ while (index < text.length) {
234
+ const attrs = getTextAttributesAt(block, index);
235
+ let end = index + 1;
236
+ while (end < text.length && sameAttrs(attrs, getTextAttributesAt(block, end))) {
237
+ end += 1;
238
+ }
239
+ const segment = text.slice(index, end);
240
+ xmlText.insert(yIndex, segment, attrs);
241
+ yIndex += segment.length;
242
+ index = end;
243
+ }
244
+ }
245
+ function getTextAttributesAt(block, offset) {
246
+ const styles = (block.inlineStyleRanges ?? [])
247
+ .filter((range) => offset >= range.offset && offset < range.offset + range.length)
248
+ .map((range) => range.style)
249
+ .sort();
250
+ const entity = (block.entityRanges ?? []).find((range) => offset >= range.offset && offset < range.offset + range.length);
251
+ const attrs = {};
252
+ if (styles.length > 0)
253
+ attrs.draftStyles = styles.join(',');
254
+ if (entity)
255
+ attrs.draftEntity = String(entity.key);
256
+ return attrs;
257
+ }
258
+ function sameAttrs(a, b) {
259
+ return a.draftStyles === b.draftStyles && a.draftEntity === b.draftEntity;
260
+ }
261
+ function readBlockFromXml(blockElement) {
262
+ try {
263
+ const textNode = blockElement.toArray().find((node) => node instanceof Y.XmlText);
264
+ const text = textNode?.toString() ?? '';
265
+ const dataAttr = blockElement.getAttribute('data');
266
+ const entityAttr = blockElement.getAttribute('entityRanges');
267
+ return {
268
+ key: String(blockElement.getAttribute('key') || `b${Math.random().toString(36).slice(2, 7)}`),
269
+ text,
270
+ type: String(blockElement.getAttribute('type') || 'unstyled'),
271
+ depth: Number(blockElement.getAttribute('depth') || 0),
272
+ inlineStyleRanges: textNode ? readInlineStyleRanges(textNode) : [],
273
+ entityRanges: typeof entityAttr === 'string' ? JSON.parse(entityAttr) : [],
274
+ data: typeof dataAttr === 'string' ? JSON.parse(dataAttr) : {},
275
+ };
276
+ }
277
+ catch (error) {
278
+ logError('Failed to read Draft.js block from XML', error);
279
+ return null;
280
+ }
281
+ }
282
+ function readInlineStyleRanges(xmlText) {
283
+ const ranges = [];
284
+ let offset = 0;
285
+ for (const op of xmlText.toDelta()) {
286
+ const insert = typeof op.insert === 'string' ? op.insert : '';
287
+ const styles = op.attributes?.draftStyles?.split(',').filter(Boolean) ?? [];
288
+ for (const style of styles) {
289
+ ranges.push({ offset, length: insert.length, style: style });
290
+ }
291
+ offset += insert.length;
292
+ }
293
+ return ranges;
294
+ }
295
+ function rawBlockFromText(text, index) {
296
+ if (!text)
297
+ return null;
298
+ return {
299
+ key: stableDraftBlockKey(text, index),
300
+ text,
301
+ type: 'unstyled',
302
+ depth: 0,
303
+ inlineStyleRanges: [],
304
+ entityRanges: [],
305
+ data: {},
306
+ };
307
+ }
308
+ function stableDraftBlockKey(text, index) {
309
+ let hash = 0;
310
+ for (let i = 0; i < text.length; i += 1) {
311
+ hash = ((hash << 5) - hash + text.charCodeAt(i)) | 0;
312
+ }
313
+ return `g${index}${Math.abs(hash).toString(36).slice(0, 4)}`.slice(0, 5);
314
+ }
315
+ function isXmlElement(node, nodeName) {
316
+ return node instanceof Y.XmlElement && node.nodeName === nodeName;
317
+ }
318
+ function readTextFromXmlNode(node) {
319
+ try {
320
+ if (node instanceof Y.XmlText) {
321
+ return node.toString();
322
+ }
323
+ if (node instanceof Y.XmlElement || node instanceof Y.XmlFragment) {
324
+ const childText = node.toArray().map(readTextFromXmlNode).join('');
325
+ if (childText)
326
+ return childText;
327
+ return stripXmlTags(node.toString());
328
+ }
329
+ return '';
330
+ }
331
+ catch {
332
+ return '';
333
+ }
334
+ }
335
+ function stripXmlTags(value) {
336
+ return value.replace(/<[^>]+>/g, '');
337
+ }
338
+ function rawFromDraftMetaString(value) {
339
+ try {
340
+ const rawStart = value.indexOf('raw="');
341
+ if (rawStart === -1)
342
+ return null;
343
+ const contentStart = rawStart + 'raw="'.length;
344
+ let contentEnd = value.indexOf('" schema=', contentStart);
345
+ if (contentEnd === -1) {
346
+ contentEnd = value.indexOf('"></draftjs-meta>', contentStart);
347
+ }
348
+ if (contentEnd === -1)
349
+ return null;
350
+ const rawJson = value.slice(contentStart, contentEnd).replace(/&quot;/g, '"');
351
+ const parsed = JSON.parse(rawJson);
352
+ return isRawDraftContentState(parsed) ? parsed : null;
353
+ }
354
+ catch {
355
+ return null;
356
+ }
357
+ }
358
+
359
+ // POLICY: Public package methods must never throw into consumer code.
360
+ const CONTENT_KEY = 'draftjs';
361
+ const STORE_SOURCE = 'draftjs';
362
+ const DEFAULT_REST_CONTENT_KEY = 'document-store';
363
+ const CONFIG_MAP_KEY = 'veltConfig';
364
+ const FORCE_RESET_REVISION_KEY = '__draftjsForceResetRevision';
365
+ const FORCE_RESET_OPEN_FOR_EDITS_KEY = '__draftjsForceResetOpenForEdits';
366
+ const VERSION_META_MAP_KEY = 'draftjsVersionMetadata';
367
+ const VERSION_META_LAST_CHANGE_KEY = 'lastChange';
368
+ class CollaborationManager {
369
+ constructor(config) {
370
+ this.store = null;
371
+ this.provider = null;
372
+ this.xmlFragment = null;
373
+ this.restXmlFragment = null;
374
+ this.versionMetadata = null;
375
+ this.undoManager = null;
376
+ this.userId = 'anonymous';
377
+ this.initializedState = false;
378
+ this.syncedState = false;
379
+ this.statusState = 'connecting';
380
+ this.initialContentApplied = false;
381
+ this.applyingRemote = false;
382
+ this.skipNextSync = false;
383
+ this.skipNextSyncToken = 0;
384
+ this.lastRawJson = '';
385
+ this.forceResetRawContent = null;
386
+ this.forceResetRawJson = '';
387
+ this.hasLocalEditAfterForceReset = false;
388
+ this.forceResetStrict = false;
389
+ this.localOrigin = { source: STORE_SOURCE, direction: 'local' };
390
+ this.initialOrigin = { source: STORE_SOURCE, direction: 'initial' };
391
+ this.restBridgeOrigin = { source: STORE_SOURCE, direction: 'rest-bridge' };
392
+ this.statusListeners = new Set();
393
+ this.syncedListeners = new Set();
394
+ this.awarenessListeners = new Set();
395
+ this.versionListeners = new Set();
396
+ this.cleanupFns = [];
397
+ this.snapshotSaveTimer = null;
398
+ this.config = config;
399
+ }
400
+ async initialize() {
401
+ try {
402
+ this.userId = this.resolveUserId();
403
+ this.store = this.createStore();
404
+ this.xmlFragment = this.store.getXml();
405
+ this.restXmlFragment = this.getRestXmlFragment();
406
+ this.versionMetadata = this.store.getDoc().getMap(VERSION_META_MAP_KEY);
407
+ this.bindXmlFragment();
408
+ this.bindRestXmlFragment();
409
+ this.setupVersionMetadataListener();
410
+ this.createUndoManager();
411
+ try {
412
+ await this.store.initialize();
413
+ }
414
+ catch (error) {
415
+ if (this.isCorruptionError(error)) {
416
+ await this.tryRecoverFromCorruption();
417
+ }
418
+ else {
419
+ this.reportError('Store initialization failed, entering degraded state', error);
420
+ }
421
+ }
422
+ try {
423
+ this.provider = this.store?.getProvider() ?? null;
424
+ }
425
+ catch (error) {
426
+ this.reportError('Failed to get provider', error);
427
+ }
428
+ this.setupRestDataFallbackListener();
429
+ this.applyRestXmlFragmentToDraft(false);
430
+ await this.tryHydrateRestDataFallback();
431
+ this.setupProviderListeners();
432
+ this.setupAwarenessListeners();
433
+ this.tryApplyInitialContent();
434
+ this.applyFragmentToEditor(false);
435
+ this.sendCursorPosition();
436
+ this.initializedState = true;
437
+ }
438
+ catch (error) {
439
+ this.reportError('Initialization failed unexpectedly', error);
440
+ }
441
+ }
442
+ destroy() {
443
+ try {
444
+ for (const fn of this.cleanupFns.splice(0)) {
445
+ try {
446
+ fn();
447
+ }
448
+ catch {
449
+ // ignore cleanup errors
450
+ }
451
+ }
452
+ this.statusListeners.clear();
453
+ this.syncedListeners.clear();
454
+ this.awarenessListeners.clear();
455
+ this.versionListeners.clear();
456
+ if (this.snapshotSaveTimer) {
457
+ clearTimeout(this.snapshotSaveTimer);
458
+ this.snapshotSaveTimer = null;
459
+ }
460
+ this.undoManager?.destroy();
461
+ this.undoManager = null;
462
+ try {
463
+ this.store?.updateAwareness(null);
464
+ }
465
+ catch {
466
+ // ignore awareness cleanup errors
467
+ }
468
+ this.store?.destroy();
469
+ this.store = null;
470
+ this.provider = null;
471
+ this.xmlFragment = null;
472
+ this.restXmlFragment = null;
473
+ this.versionMetadata = null;
474
+ this.initializedState = false;
475
+ this.syncedState = false;
476
+ this.statusState = 'disconnected';
477
+ }
478
+ catch (error) {
479
+ logError('Error during destroy', error);
480
+ }
481
+ }
482
+ handleChange(nextState) {
483
+ try {
484
+ if (this.skipNextSync) {
485
+ this.skipNextSync = false;
486
+ this.skipNextSyncToken += 1;
487
+ this.updateAwarenessFromEditorState(nextState);
488
+ return nextState;
489
+ }
490
+ if (!this.applyingRemote) {
491
+ this.syncEditorStateToXml(nextState);
492
+ }
493
+ this.updateAwarenessFromEditorState(nextState);
494
+ return nextState;
495
+ }
496
+ catch (error) {
497
+ this.reportError('Failed to handle Draft.js change', error);
498
+ return nextState;
499
+ }
500
+ }
501
+ sendCursorPosition(selection) {
502
+ try {
503
+ const resolvedSelection = selection ?? this.safeGetEditorState()?.getSelection();
504
+ this.updateAwarenessSelection(resolvedSelection ?? null);
505
+ }
506
+ catch (error) {
507
+ this.reportError('Failed to publish Draft.js cursor position', error);
508
+ }
509
+ }
510
+ getRemoteCursors() {
511
+ try {
512
+ return this.readRemoteCursors();
513
+ }
514
+ catch (error) {
515
+ this.reportError('Failed to read remote cursors', error);
516
+ return [];
517
+ }
518
+ }
519
+ onRemoteCursorsChange(cb) {
520
+ try {
521
+ this.awarenessListeners.add(cb);
522
+ cb(this.readRemoteCursors());
523
+ }
524
+ catch (error) {
525
+ this.reportError('Failed to add remote cursor listener', error);
526
+ }
527
+ return () => {
528
+ try {
529
+ this.awarenessListeners.delete(cb);
530
+ }
531
+ catch {
532
+ // ignore listener cleanup errors
533
+ }
534
+ };
535
+ }
536
+ get initialized() {
537
+ return this.initializedState;
538
+ }
539
+ get synced() {
540
+ return this.syncedState;
541
+ }
542
+ get status() {
543
+ return this.statusState;
544
+ }
545
+ onStatusChange(cb) {
546
+ try {
547
+ this.statusListeners.add(cb);
548
+ cb(this.statusState);
549
+ }
550
+ catch (error) {
551
+ this.reportError('Failed to add status listener', error);
552
+ }
553
+ return () => {
554
+ try {
555
+ this.statusListeners.delete(cb);
556
+ }
557
+ catch {
558
+ // ignore listener cleanup errors
559
+ }
560
+ };
561
+ }
562
+ onSynced(cb) {
563
+ try {
564
+ this.syncedListeners.add(cb);
565
+ cb(this.syncedState);
566
+ }
567
+ catch (error) {
568
+ this.reportError('Failed to add synced listener', error);
569
+ }
570
+ return () => {
571
+ try {
572
+ this.syncedListeners.delete(cb);
573
+ }
574
+ catch {
575
+ // ignore listener cleanup errors
576
+ }
577
+ };
578
+ }
579
+ getDoc() {
580
+ try {
581
+ return this.store?.getDoc() ?? null;
582
+ }
583
+ catch (error) {
584
+ this.reportError('Failed to get Y.Doc', error);
585
+ return null;
586
+ }
587
+ }
588
+ getXmlFragment() {
589
+ try {
590
+ return this.xmlFragment ?? this.store?.getXml() ?? null;
591
+ }
592
+ catch (error) {
593
+ this.reportError('Failed to get XML fragment', error);
594
+ return null;
595
+ }
596
+ }
597
+ getXmlText() {
598
+ try {
599
+ const fragment = this.getXmlFragment();
600
+ const firstBlock = fragment?.toArray().find((node) => {
601
+ return node instanceof Y.XmlElement && node.nodeName === 'draftjs-block';
602
+ });
603
+ return firstBlock?.toArray().find((node) => node instanceof Y.XmlText) ?? null;
604
+ }
605
+ catch (error) {
606
+ this.reportError('Failed to get XML text', error);
607
+ return null;
608
+ }
609
+ }
610
+ getProvider() {
611
+ try {
612
+ return this.provider ?? this.store?.getProvider() ?? null;
613
+ }
614
+ catch (error) {
615
+ this.reportError('Failed to get SyncProvider', error);
616
+ return null;
617
+ }
618
+ }
619
+ getAwareness() {
620
+ try {
621
+ return this.store?.getAwareness() ?? this.provider?.getAwareness() ?? null;
622
+ }
623
+ catch (error) {
624
+ this.reportError('Failed to get Awareness', error);
625
+ return null;
626
+ }
627
+ }
628
+ getUndoManager() {
629
+ try {
630
+ return this.undoManager;
631
+ }
632
+ catch (error) {
633
+ this.reportError('Failed to get UndoManager', error);
634
+ return null;
635
+ }
636
+ }
637
+ getStore() {
638
+ try {
639
+ return this.store;
640
+ }
641
+ catch (error) {
642
+ this.reportError('Failed to get Store', error);
643
+ return null;
644
+ }
645
+ }
646
+ async saveVersion(name) {
647
+ try {
648
+ this.flushCurrentEditorState();
649
+ const versionId = (await this.store?.saveVersion(name)) ?? '';
650
+ if (versionId) {
651
+ this.publishVersionChange('save', versionId, name);
652
+ }
653
+ return versionId;
654
+ }
655
+ catch (error) {
656
+ this.reportError('Failed to save version', error);
657
+ return '';
658
+ }
659
+ }
660
+ async getVersions() {
661
+ try {
662
+ return (await this.store?.getVersions()) ?? [];
663
+ }
664
+ catch (error) {
665
+ this.reportError('Failed to get versions', error);
666
+ return [];
667
+ }
668
+ }
669
+ async restoreVersion(versionId) {
670
+ try {
671
+ const restored = (await this.store?.restoreVersion(versionId)) ?? false;
672
+ if (restored) {
673
+ this.mirrorCurrentDraftToRestXml(this.restBridgeOrigin);
674
+ this.applyFragmentToEditor(false);
675
+ this.store?.notifySubscribers();
676
+ await this.saveCurrentSnapshot();
677
+ this.publishVersionChange('restore', versionId);
678
+ }
679
+ return restored;
680
+ }
681
+ catch (error) {
682
+ this.reportError('Failed to restore version', error);
683
+ return false;
684
+ }
685
+ }
686
+ async setStateFromVersion(version) {
687
+ try {
688
+ await this.store?.setStateFromVersion(version);
689
+ this.mirrorCurrentDraftToRestXml(this.restBridgeOrigin);
690
+ this.applyFragmentToEditor(false);
691
+ this.store?.notifySubscribers();
692
+ await this.saveCurrentSnapshot();
693
+ this.publishVersionChange('set-state', version.versionId, version.versionName);
694
+ }
695
+ catch (error) {
696
+ this.reportError('Failed to set state from version', error);
697
+ }
698
+ }
699
+ onVersionsChange(cb) {
700
+ try {
701
+ this.versionListeners.add(cb);
702
+ }
703
+ catch (error) {
704
+ this.reportError('Failed to add version listener', error);
705
+ }
706
+ return () => {
707
+ try {
708
+ this.versionListeners.delete(cb);
709
+ }
710
+ catch {
711
+ // ignore listener cleanup errors
712
+ }
713
+ };
714
+ }
715
+ createStore() {
716
+ return new Store({
717
+ id: this.config.editorId,
718
+ userId: this.userId,
719
+ type: 'xml',
720
+ contentKey: CONTENT_KEY,
721
+ debounceMs: this.config.debounceMs,
722
+ veltClient: this.config.veltClient,
723
+ source: STORE_SOURCE,
724
+ forceResetInitialContent: this.config.forceResetInitialContent,
725
+ });
726
+ }
727
+ bindXmlFragment() {
728
+ try {
729
+ if (!this.xmlFragment)
730
+ return;
731
+ const observer = (_events, transaction) => {
732
+ try {
733
+ if (transaction.origin === this.localOrigin || transaction.origin === this.initialOrigin) {
734
+ return;
735
+ }
736
+ if (transaction.origin === this.restBridgeOrigin) {
737
+ return;
738
+ }
739
+ if (this.enforceForceResetInitialContent(true)) {
740
+ return;
741
+ }
742
+ this.applyFragmentToEditor(true);
743
+ }
744
+ catch (error) {
745
+ this.reportError('Failed to apply remote XML update', error);
746
+ }
747
+ };
748
+ this.xmlFragment.observeDeep(observer);
749
+ this.cleanupFns.push(() => {
750
+ try {
751
+ this.xmlFragment?.unobserveDeep(observer);
752
+ }
753
+ catch {
754
+ // ignore observer cleanup errors
755
+ }
756
+ });
757
+ }
758
+ catch (error) {
759
+ this.reportError('Failed to bind XML fragment observer', error);
760
+ }
761
+ }
762
+ createUndoManager() {
763
+ try {
764
+ if (!this.xmlFragment)
765
+ return;
766
+ this.undoManager = new Y.UndoManager(this.xmlFragment, {
767
+ trackedOrigins: new Set([this.localOrigin]),
768
+ });
769
+ }
770
+ catch (error) {
771
+ this.reportError('Failed to create UndoManager', error);
772
+ }
773
+ }
774
+ bindRestXmlFragment() {
775
+ try {
776
+ if (!this.restXmlFragment)
777
+ return;
778
+ const observer = (_events, transaction) => {
779
+ try {
780
+ if (transaction.origin === this.localOrigin
781
+ || transaction.origin === this.initialOrigin
782
+ || transaction.origin === this.restBridgeOrigin) {
783
+ return;
784
+ }
785
+ if (this.enforceForceResetInitialContent(true)) {
786
+ return;
787
+ }
788
+ this.applyRestXmlFragmentToDraft(true, true);
789
+ }
790
+ catch (error) {
791
+ this.reportError('Failed to apply REST XML update', error);
792
+ }
793
+ };
794
+ this.restXmlFragment.observeDeep(observer);
795
+ this.cleanupFns.push(() => {
796
+ try {
797
+ this.restXmlFragment?.unobserveDeep(observer);
798
+ }
799
+ catch {
800
+ // ignore observer cleanup errors
801
+ }
802
+ });
803
+ }
804
+ catch (error) {
805
+ this.reportError('Failed to bind REST XML fragment observer', error);
806
+ }
807
+ }
808
+ setupVersionMetadataListener() {
809
+ try {
810
+ if (!this.versionMetadata)
811
+ return;
812
+ const observer = () => {
813
+ try {
814
+ const event = this.readVersionChangeEvent();
815
+ if (!event)
816
+ return;
817
+ this.versionListeners.forEach((cb) => {
818
+ try {
819
+ cb(event);
820
+ }
821
+ catch {
822
+ // ignore listener errors
823
+ }
824
+ });
825
+ }
826
+ catch (error) {
827
+ this.reportError('Failed to publish version metadata change', error);
828
+ }
829
+ };
830
+ this.versionMetadata.observe(observer);
831
+ this.cleanupFns.push(() => {
832
+ try {
833
+ this.versionMetadata?.unobserve(observer);
834
+ }
835
+ catch {
836
+ // ignore observer cleanup errors
837
+ }
838
+ });
839
+ }
840
+ catch (error) {
841
+ this.reportError('Failed to setup version metadata listener', error);
842
+ }
843
+ }
844
+ syncEditorStateToXml(editorState) {
845
+ try {
846
+ if (!this.xmlFragment)
847
+ return;
848
+ const rawContent = rawFromEditorState(editorState);
849
+ if (!rawContent)
850
+ return;
851
+ const nextRawJson = serializeRawContent(rawContent);
852
+ if (nextRawJson && nextRawJson === this.lastRawJson) {
853
+ return;
854
+ }
855
+ if (this.config.forceResetInitialContent
856
+ && this.forceResetRawJson
857
+ && nextRawJson !== this.forceResetRawJson) {
858
+ this.hasLocalEditAfterForceReset = true;
859
+ }
860
+ const shouldOpenForceResetForEdits = this.shouldOpenForceResetForEdits(nextRawJson);
861
+ const doc = this.store?.getDoc();
862
+ if (doc) {
863
+ doc.transact(() => {
864
+ writeRawToXmlFragment(this.xmlFragment, rawContent);
865
+ if (this.restXmlFragment) {
866
+ writeRawToXmlFragment(this.restXmlFragment, rawContent);
867
+ }
868
+ if (shouldOpenForceResetForEdits) {
869
+ this.markForceResetOpenForEdits(doc);
870
+ }
871
+ }, this.localOrigin);
872
+ }
873
+ else {
874
+ writeRawToXmlFragment(this.xmlFragment, rawContent);
875
+ if (this.restXmlFragment) {
876
+ writeRawToXmlFragment(this.restXmlFragment, rawContent);
877
+ }
878
+ }
879
+ this.lastRawJson = nextRawJson;
880
+ this.store?.notifySubscribers();
881
+ this.scheduleSnapshotSave();
882
+ }
883
+ catch (error) {
884
+ this.reportError('Failed to sync Draft.js state to XML', error);
885
+ }
886
+ }
887
+ flushCurrentEditorState() {
888
+ try {
889
+ const editorState = this.safeGetEditorState();
890
+ if (editorState) {
891
+ this.syncEditorStateToXml(editorState);
892
+ }
893
+ }
894
+ catch (error) {
895
+ this.reportError('Failed to flush Draft.js state', error);
896
+ }
897
+ }
898
+ applyFragmentToEditor(preserveSelection) {
899
+ try {
900
+ if (!this.xmlFragment)
901
+ return;
902
+ const rawContent = rawFromXmlFragment(this.xmlFragment);
903
+ if (!rawContent)
904
+ return;
905
+ const currentState = this.safeGetEditorState();
906
+ const currentRaw = currentState ? rawFromEditorState(currentState) : null;
907
+ if (rawContentsEqual(currentRaw, rawContent)) {
908
+ this.lastRawJson = serializeRawContent(rawContent);
909
+ return;
910
+ }
911
+ this.applyingRemote = true;
912
+ this.skipNextSync = true;
913
+ const skipToken = this.skipNextSyncToken + 1;
914
+ this.skipNextSyncToken = skipToken;
915
+ const nextState = editorStateFromRaw(rawContent, preserveSelection ? currentState : null);
916
+ if (nextState) {
917
+ this.config.setEditorState(nextState);
918
+ }
919
+ this.lastRawJson = serializeRawContent(rawContent);
920
+ setTimeout(() => {
921
+ if (this.skipNextSyncToken === skipToken) {
922
+ this.skipNextSync = false;
923
+ }
924
+ }, 0);
925
+ }
926
+ catch (error) {
927
+ this.reportError('Failed to hydrate Draft.js editor from XML', error);
928
+ }
929
+ finally {
930
+ this.applyingRemote = false;
931
+ }
932
+ }
933
+ tryApplyInitialContent() {
934
+ try {
935
+ if (this.initialContentApplied || !this.store || !this.xmlFragment)
936
+ return;
937
+ const initial = normalizeInitialContent(this.config.initialContent);
938
+ if (!initial)
939
+ return;
940
+ const preResetRaw = this.config.forceResetInitialContent
941
+ ? rawFromXmlFragment(this.xmlFragment)
942
+ : null;
943
+ this.initialContentApplied = applyInitialContent(this.store.getDoc(), this.xmlFragment, initial, this.config.forceResetInitialContent, this.initialOrigin);
944
+ if (this.initialContentApplied) {
945
+ this.lastRawJson = serializeRawContent(initial);
946
+ if (this.config.forceResetInitialContent) {
947
+ this.forceResetRawContent = initial;
948
+ this.forceResetRawJson = serializeRawContent(initial);
949
+ this.hasLocalEditAfterForceReset = false;
950
+ this.forceResetStrict = !!preResetRaw && !rawDraftContentSemanticallyEqual(preResetRaw, initial);
951
+ this.markForceResetBaseline(true);
952
+ this.mirrorRawToRestXml(initial, this.initialOrigin);
953
+ void this.saveCurrentSnapshot();
954
+ }
955
+ this.applyFragmentToEditor(false);
956
+ this.store.notifySubscribers();
957
+ }
958
+ }
959
+ catch (error) {
960
+ this.reportError('Failed to apply initial content', error);
961
+ }
962
+ }
963
+ setupProviderListeners() {
964
+ try {
965
+ if (!this.provider)
966
+ return;
967
+ const onSynced = (synced) => {
968
+ try {
969
+ this.syncedState = synced;
970
+ this.syncedListeners.forEach((cb) => {
971
+ try {
972
+ cb(synced);
973
+ }
974
+ catch {
975
+ // ignore listener errors
976
+ }
977
+ });
978
+ if (synced) {
979
+ this.applyRestXmlFragmentToDraft(true);
980
+ this.tryApplyInitialContent();
981
+ if (!this.enforceForceResetInitialContent(true)) {
982
+ this.applyFragmentToEditor(true);
983
+ }
984
+ }
985
+ }
986
+ catch (error) {
987
+ this.reportError('Error in synced handler', error);
988
+ }
989
+ };
990
+ const onStatus = (payload) => {
991
+ try {
992
+ const status = typeof payload === 'string' ? payload : payload.status;
993
+ this.statusState = status;
994
+ this.statusListeners.forEach((cb) => {
995
+ try {
996
+ cb(status);
997
+ }
998
+ catch {
999
+ // ignore listener errors
1000
+ }
1001
+ });
1002
+ }
1003
+ catch (error) {
1004
+ this.reportError('Error in status handler', error);
1005
+ }
1006
+ };
1007
+ this.provider.on('synced', onSynced);
1008
+ this.provider.on('status', onStatus);
1009
+ this.cleanupFns.push(() => {
1010
+ this.provider?.off('synced', onSynced);
1011
+ this.provider?.off('status', onStatus);
1012
+ });
1013
+ this.syncedState = this.provider.synced;
1014
+ this.statusState = this.provider.status;
1015
+ }
1016
+ catch (error) {
1017
+ this.reportError('Failed to setup provider listeners', error);
1018
+ }
1019
+ }
1020
+ setupAwarenessListeners() {
1021
+ try {
1022
+ const awareness = this.getAwareness();
1023
+ if (!awareness)
1024
+ return;
1025
+ const onChange = () => {
1026
+ try {
1027
+ const cursors = this.readRemoteCursors();
1028
+ this.awarenessListeners.forEach((cb) => {
1029
+ try {
1030
+ cb(cursors);
1031
+ }
1032
+ catch {
1033
+ // ignore listener errors
1034
+ }
1035
+ });
1036
+ }
1037
+ catch (error) {
1038
+ this.reportError('Failed to publish awareness change', error);
1039
+ }
1040
+ };
1041
+ awareness.on('change', onChange);
1042
+ this.cleanupFns.push(() => {
1043
+ try {
1044
+ awareness.off('change', onChange);
1045
+ }
1046
+ catch {
1047
+ // ignore listener cleanup errors
1048
+ }
1049
+ });
1050
+ }
1051
+ catch (error) {
1052
+ this.reportError('Failed to setup awareness listeners', error);
1053
+ }
1054
+ }
1055
+ setupRestDataFallbackListener() {
1056
+ try {
1057
+ const crdtElement = this.getCrdtElement();
1058
+ if (!crdtElement?.onDataChange)
1059
+ return;
1060
+ const unsubscribe = crdtElement.onDataChange({
1061
+ id: this.config.editorId,
1062
+ callback: (payload) => {
1063
+ try {
1064
+ this.applyRestDataFallback(payload);
1065
+ }
1066
+ catch (error) {
1067
+ this.reportError('Failed to apply REST data update', error);
1068
+ }
1069
+ },
1070
+ });
1071
+ if (typeof unsubscribe === 'function') {
1072
+ this.cleanupFns.push(unsubscribe);
1073
+ }
1074
+ }
1075
+ catch (error) {
1076
+ this.reportError('Failed to setup REST data fallback listener', error);
1077
+ }
1078
+ }
1079
+ async tryHydrateRestDataFallback() {
1080
+ try {
1081
+ if (!this.xmlFragment)
1082
+ return;
1083
+ this.applyRestXmlFragmentToDraft(false);
1084
+ const crdtElement = this.getCrdtElement();
1085
+ if (!crdtElement?.getData)
1086
+ return;
1087
+ const payload = await crdtElement.getData({ id: this.config.editorId });
1088
+ this.applyRestDataFallback(payload);
1089
+ }
1090
+ catch (error) {
1091
+ this.reportError('Failed to hydrate REST data fallback', error);
1092
+ }
1093
+ }
1094
+ applyRestDataFallback(payload) {
1095
+ try {
1096
+ if (!this.xmlFragment)
1097
+ return;
1098
+ if (this.enforceForceResetInitialContent(false)) {
1099
+ return;
1100
+ }
1101
+ const rawData = extractRestDataString(payload);
1102
+ if (!rawData)
1103
+ return;
1104
+ const rawContent = rawFromXmlLikeString(rawData);
1105
+ if (!rawContent)
1106
+ return;
1107
+ const currentRaw = rawFromXmlFragment(this.xmlFragment);
1108
+ if (rawContentsEqual(currentRaw, rawContent))
1109
+ return;
1110
+ const doc = this.store?.getDoc();
1111
+ if (doc) {
1112
+ doc.transact(() => {
1113
+ writeRawToXmlFragment(this.xmlFragment, rawContent);
1114
+ }, 'remote-rest-data');
1115
+ }
1116
+ else {
1117
+ writeRawToXmlFragment(this.xmlFragment, rawContent);
1118
+ }
1119
+ this.lastRawJson = serializeRawContent(rawContent);
1120
+ this.applyFragmentToEditor(false);
1121
+ this.store?.notifySubscribers();
1122
+ }
1123
+ catch (error) {
1124
+ this.reportError('Failed to apply REST data fallback', error);
1125
+ }
1126
+ }
1127
+ applyRestXmlFragmentToDraft(preserveSelection, force = false) {
1128
+ try {
1129
+ if (!this.xmlFragment || !this.restXmlFragment || this.restXmlFragment.length === 0) {
1130
+ return false;
1131
+ }
1132
+ if (this.enforceForceResetInitialContent(preserveSelection)) {
1133
+ return true;
1134
+ }
1135
+ if (!force && this.xmlFragment.length > 0) {
1136
+ return false;
1137
+ }
1138
+ const rawContent = rawFromXmlFragment(this.restXmlFragment)
1139
+ ?? rawFromXmlLikeString(this.restXmlFragment.toString());
1140
+ if (!rawContent)
1141
+ return false;
1142
+ const currentRaw = rawFromXmlFragment(this.xmlFragment);
1143
+ if (rawContentsEqual(currentRaw, rawContent))
1144
+ return false;
1145
+ const doc = this.store?.getDoc();
1146
+ if (doc) {
1147
+ doc.transact(() => {
1148
+ writeRawToXmlFragment(this.xmlFragment, rawContent);
1149
+ }, this.restBridgeOrigin);
1150
+ }
1151
+ else {
1152
+ writeRawToXmlFragment(this.xmlFragment, rawContent);
1153
+ }
1154
+ this.lastRawJson = serializeRawContent(rawContent);
1155
+ this.applyFragmentToEditor(preserveSelection);
1156
+ this.store?.notifySubscribers();
1157
+ return true;
1158
+ }
1159
+ catch (error) {
1160
+ this.reportError('Failed to bridge REST XML content', error);
1161
+ return false;
1162
+ }
1163
+ }
1164
+ updateAwarenessFromEditorState(editorState) {
1165
+ try {
1166
+ this.updateAwarenessSelection(editorState.getSelection());
1167
+ }
1168
+ catch (error) {
1169
+ this.reportError('Failed to update awareness from editor state', error);
1170
+ }
1171
+ }
1172
+ scheduleSnapshotSave() {
1173
+ try {
1174
+ if (this.snapshotSaveTimer) {
1175
+ clearTimeout(this.snapshotSaveTimer);
1176
+ }
1177
+ const delay = Math.max(100, Math.min(this.config.debounceMs ?? 250, 1000));
1178
+ this.snapshotSaveTimer = setTimeout(() => {
1179
+ this.snapshotSaveTimer = null;
1180
+ void this.saveCurrentSnapshot();
1181
+ }, delay);
1182
+ }
1183
+ catch (error) {
1184
+ this.reportError('Failed to schedule CRDT snapshot save', error);
1185
+ }
1186
+ }
1187
+ enforceForceResetInitialContent(preserveSelection) {
1188
+ try {
1189
+ if (!this.config.forceResetInitialContent
1190
+ || this.hasLocalEditAfterForceReset
1191
+ || !this.forceResetStrict
1192
+ || this.isForceResetOpenForEdits()
1193
+ || !this.forceResetRawContent
1194
+ || !this.xmlFragment) {
1195
+ return false;
1196
+ }
1197
+ const currentRaw = rawFromXmlFragment(this.xmlFragment);
1198
+ if (rawContentsEqual(currentRaw, this.forceResetRawContent)) {
1199
+ return false;
1200
+ }
1201
+ const doc = this.store?.getDoc();
1202
+ if (doc) {
1203
+ doc.transact(() => {
1204
+ writeRawToXmlFragment(this.xmlFragment, this.forceResetRawContent);
1205
+ if (this.restXmlFragment) {
1206
+ writeRawToXmlFragment(this.restXmlFragment, this.forceResetRawContent);
1207
+ }
1208
+ this.markForceResetBaseline(false, doc);
1209
+ }, this.initialOrigin);
1210
+ }
1211
+ else {
1212
+ writeRawToXmlFragment(this.xmlFragment, this.forceResetRawContent);
1213
+ if (this.restXmlFragment) {
1214
+ writeRawToXmlFragment(this.restXmlFragment, this.forceResetRawContent);
1215
+ }
1216
+ this.markForceResetBaseline(false);
1217
+ }
1218
+ this.lastRawJson = this.forceResetRawJson;
1219
+ this.applyFragmentToEditor(preserveSelection);
1220
+ this.store?.notifySubscribers();
1221
+ void this.saveCurrentSnapshot();
1222
+ return true;
1223
+ }
1224
+ catch (error) {
1225
+ this.reportError('Failed to enforce forceResetInitialContent', error);
1226
+ return false;
1227
+ }
1228
+ }
1229
+ shouldOpenForceResetForEdits(nextRawJson) {
1230
+ try {
1231
+ if (!nextRawJson)
1232
+ return false;
1233
+ const doc = this.store?.getDoc();
1234
+ if (!doc)
1235
+ return false;
1236
+ const configMap = doc.getMap(CONFIG_MAP_KEY);
1237
+ if (!configMap.has(FORCE_RESET_REVISION_KEY))
1238
+ return false;
1239
+ return !this.forceResetRawJson || nextRawJson !== this.forceResetRawJson;
1240
+ }
1241
+ catch {
1242
+ return false;
1243
+ }
1244
+ }
1245
+ markForceResetBaseline(newRevision, docOverride) {
1246
+ try {
1247
+ const doc = docOverride ?? this.store?.getDoc();
1248
+ if (!doc)
1249
+ return;
1250
+ const configMap = doc.getMap(CONFIG_MAP_KEY);
1251
+ if (newRevision || !configMap.has(FORCE_RESET_REVISION_KEY)) {
1252
+ configMap.set(FORCE_RESET_REVISION_KEY, `${Date.now()}-${Math.random().toString(36).slice(2)}`);
1253
+ }
1254
+ configMap.set(FORCE_RESET_OPEN_FOR_EDITS_KEY, false);
1255
+ }
1256
+ catch (error) {
1257
+ this.reportError('Failed to mark forceReset baseline metadata', error);
1258
+ }
1259
+ }
1260
+ publishVersionChange(reason, versionId, versionName) {
1261
+ try {
1262
+ const metadata = this.versionMetadata ?? this.store?.getDoc().getMap(VERSION_META_MAP_KEY);
1263
+ if (!metadata)
1264
+ return;
1265
+ const doc = this.store?.getDoc();
1266
+ const event = {
1267
+ revision: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
1268
+ reason,
1269
+ versionId,
1270
+ versionName,
1271
+ userId: this.userId,
1272
+ timestamp: Date.now(),
1273
+ };
1274
+ if (doc) {
1275
+ doc.transact(() => {
1276
+ metadata.set(VERSION_META_LAST_CHANGE_KEY, event);
1277
+ }, { source: STORE_SOURCE, direction: 'version-metadata' });
1278
+ }
1279
+ else {
1280
+ metadata.set(VERSION_META_LAST_CHANGE_KEY, event);
1281
+ }
1282
+ }
1283
+ catch (error) {
1284
+ this.reportError('Failed to publish version metadata change', error);
1285
+ }
1286
+ }
1287
+ readVersionChangeEvent() {
1288
+ try {
1289
+ const value = this.versionMetadata?.get(VERSION_META_LAST_CHANGE_KEY);
1290
+ if (!value || typeof value !== 'object')
1291
+ return null;
1292
+ const event = value;
1293
+ if (typeof event.revision !== 'string')
1294
+ return null;
1295
+ if (event.reason !== 'save' && event.reason !== 'restore' && event.reason !== 'set-state')
1296
+ return null;
1297
+ if (typeof event.timestamp !== 'number')
1298
+ return null;
1299
+ return event;
1300
+ }
1301
+ catch {
1302
+ return null;
1303
+ }
1304
+ }
1305
+ markForceResetOpenForEdits(doc) {
1306
+ try {
1307
+ const configMap = doc.getMap(CONFIG_MAP_KEY);
1308
+ if (configMap.has(FORCE_RESET_REVISION_KEY)) {
1309
+ configMap.set(FORCE_RESET_OPEN_FOR_EDITS_KEY, true);
1310
+ }
1311
+ }
1312
+ catch (error) {
1313
+ this.reportError('Failed to mark forceReset as open for edits', error);
1314
+ }
1315
+ }
1316
+ isForceResetOpenForEdits() {
1317
+ try {
1318
+ const doc = this.store?.getDoc();
1319
+ if (!doc)
1320
+ return false;
1321
+ return doc.getMap(CONFIG_MAP_KEY).get(FORCE_RESET_OPEN_FOR_EDITS_KEY) === true;
1322
+ }
1323
+ catch {
1324
+ return false;
1325
+ }
1326
+ }
1327
+ mirrorCurrentDraftToRestXml(origin) {
1328
+ try {
1329
+ if (!this.xmlFragment)
1330
+ return false;
1331
+ const rawContent = rawFromXmlFragment(this.xmlFragment);
1332
+ if (!rawContent)
1333
+ return false;
1334
+ return this.mirrorRawToRestXml(rawContent, origin);
1335
+ }
1336
+ catch (error) {
1337
+ this.reportError('Failed to mirror Draft.js XML to REST sidecar', error);
1338
+ return false;
1339
+ }
1340
+ }
1341
+ mirrorRawToRestXml(rawContent, origin) {
1342
+ try {
1343
+ if (!this.restXmlFragment)
1344
+ return false;
1345
+ const currentRestRaw = rawFromXmlFragment(this.restXmlFragment);
1346
+ if (rawContentsEqual(currentRestRaw, rawContent))
1347
+ return false;
1348
+ const doc = this.store?.getDoc();
1349
+ if (doc) {
1350
+ doc.transact(() => {
1351
+ writeRawToXmlFragment(this.restXmlFragment, rawContent);
1352
+ }, origin);
1353
+ }
1354
+ else {
1355
+ writeRawToXmlFragment(this.restXmlFragment, rawContent);
1356
+ }
1357
+ return true;
1358
+ }
1359
+ catch (error) {
1360
+ this.reportError('Failed to mirror Draft.js raw content to REST sidecar', error);
1361
+ return false;
1362
+ }
1363
+ }
1364
+ async saveCurrentSnapshot() {
1365
+ try {
1366
+ const doc = this.store?.getDoc();
1367
+ const crdtElement = this.getCrdtElement();
1368
+ if (!doc || !crdtElement?.saveSnapshot)
1369
+ return;
1370
+ await crdtElement.saveSnapshot({
1371
+ id: this.config.editorId,
1372
+ state: Array.from(Y.encodeStateAsUpdate(doc)),
1373
+ vector: Array.from(Y.encodeStateVector(doc)),
1374
+ type: 'xml',
1375
+ contentKey: CONTENT_KEY,
1376
+ source: STORE_SOURCE,
1377
+ });
1378
+ }
1379
+ catch (error) {
1380
+ this.reportError('Failed to save CRDT snapshot', error);
1381
+ }
1382
+ }
1383
+ updateAwarenessSelection(selection) {
1384
+ try {
1385
+ if (!this.store)
1386
+ return;
1387
+ const cursorSelection = selection ? selectionToCursor(selection) : null;
1388
+ this.store.updateAwareness({
1389
+ source: STORE_SOURCE,
1390
+ editorId: this.config.editorId,
1391
+ cursorData: this.config.cursorData,
1392
+ selection: cursorSelection,
1393
+ });
1394
+ }
1395
+ catch (error) {
1396
+ this.reportError('Failed to update awareness selection', error);
1397
+ }
1398
+ }
1399
+ readRemoteCursors() {
1400
+ try {
1401
+ const awareness = this.getAwareness();
1402
+ if (!awareness)
1403
+ return [];
1404
+ const localClientId = awareness.clientID;
1405
+ const cursors = [];
1406
+ awareness.getStates().forEach((state, clientId) => {
1407
+ if (clientId === localClientId)
1408
+ return;
1409
+ if (state?.source !== STORE_SOURCE)
1410
+ return;
1411
+ if (state?.editorId !== this.config.editorId)
1412
+ return;
1413
+ cursors.push({
1414
+ clientId,
1415
+ user: state.user,
1416
+ cursorData: state.cursorData,
1417
+ selection: state.selection,
1418
+ });
1419
+ });
1420
+ return cursors;
1421
+ }
1422
+ catch (error) {
1423
+ this.reportError('Failed to read awareness states', error);
1424
+ return [];
1425
+ }
1426
+ }
1427
+ safeGetEditorState() {
1428
+ try {
1429
+ return this.config.getEditorState();
1430
+ }
1431
+ catch (error) {
1432
+ this.reportError('Failed to get Draft.js editor state', error);
1433
+ return null;
1434
+ }
1435
+ }
1436
+ resolveUserId() {
1437
+ try {
1438
+ const user = this.config.veltClient?.getUser?.();
1439
+ return user?.userId || 'anonymous';
1440
+ }
1441
+ catch {
1442
+ return 'anonymous';
1443
+ }
1444
+ }
1445
+ getCrdtElement() {
1446
+ try {
1447
+ return this.config.veltClient?.getCrdtElement?.() ?? null;
1448
+ }
1449
+ catch {
1450
+ return null;
1451
+ }
1452
+ }
1453
+ getRestXmlFragment() {
1454
+ try {
1455
+ if (!this.store)
1456
+ return null;
1457
+ const restContentKey = this.config.restContentKey ?? DEFAULT_REST_CONTENT_KEY;
1458
+ if (!restContentKey || restContentKey === CONTENT_KEY)
1459
+ return null;
1460
+ return this.store.getDoc().getXmlFragment(restContentKey);
1461
+ }
1462
+ catch (error) {
1463
+ this.reportError('Failed to get REST XML fragment', error);
1464
+ return null;
1465
+ }
1466
+ }
1467
+ isCorruptionError(error) {
1468
+ try {
1469
+ const msg = String(error);
1470
+ return msg.includes('contentRefs') || msg.includes('Unexpected end of array');
1471
+ }
1472
+ catch {
1473
+ return false;
1474
+ }
1475
+ }
1476
+ async tryRecoverFromCorruption() {
1477
+ try {
1478
+ logError('Encountered Yjs corruption error, attempting recovery');
1479
+ this.store?.destroy();
1480
+ this.store = this.createStore();
1481
+ this.xmlFragment = this.store.getXml();
1482
+ this.bindXmlFragment();
1483
+ this.createUndoManager();
1484
+ await this.store.initialize();
1485
+ }
1486
+ catch (error) {
1487
+ this.reportError('Recovery failed, editor will function in degraded state', error);
1488
+ }
1489
+ }
1490
+ reportError(message, error) {
1491
+ try {
1492
+ logError(message, error);
1493
+ this.config.onError?.(error instanceof Error ? error : new Error(String(error)));
1494
+ }
1495
+ catch {
1496
+ // Never throw while reporting errors.
1497
+ }
1498
+ }
1499
+ }
1500
+ async function createCollaboration(config) {
1501
+ try {
1502
+ const manager = new CollaborationManager(config);
1503
+ await manager.initialize();
1504
+ return manager;
1505
+ }
1506
+ catch (error) {
1507
+ logError('Failed to create collaboration', error);
1508
+ return new CollaborationManager(config);
1509
+ }
1510
+ }
1511
+ function selectionToCursor(selection) {
1512
+ return {
1513
+ anchorKey: selection.getAnchorKey(),
1514
+ anchorOffset: selection.getAnchorOffset(),
1515
+ focusKey: selection.getFocusKey(),
1516
+ focusOffset: selection.getFocusOffset(),
1517
+ isBackward: selection.getIsBackward(),
1518
+ hasFocus: selection.getHasFocus(),
1519
+ };
1520
+ }
1521
+ function extractRestDataString(payload) {
1522
+ try {
1523
+ if (!payload || typeof payload !== 'object')
1524
+ return null;
1525
+ const data = payload.data;
1526
+ if (typeof data === 'string')
1527
+ return data;
1528
+ const nested = payload.result?.data;
1529
+ if (Array.isArray(nested) && typeof nested[0]?.data === 'string')
1530
+ return nested[0].data;
1531
+ return null;
1532
+ }
1533
+ catch {
1534
+ return null;
1535
+ }
1536
+ }
1537
+ function rawDraftContentSemanticallyEqual(a, b) {
1538
+ try {
1539
+ if (!a || !b || a.blocks.length !== b.blocks.length)
1540
+ return false;
1541
+ return a.blocks.every((block, index) => {
1542
+ const other = b.blocks[index];
1543
+ return !!other
1544
+ && block.text === other.text
1545
+ && block.type === other.type
1546
+ && (block.depth ?? 0) === (other.depth ?? 0)
1547
+ && JSON.stringify(block.inlineStyleRanges ?? []) === JSON.stringify(other.inlineStyleRanges ?? [])
1548
+ && JSON.stringify(block.entityRanges ?? []) === JSON.stringify(other.entityRanges ?? [])
1549
+ && JSON.stringify(block.data ?? {}) === JSON.stringify(other.data ?? {});
1550
+ });
1551
+ }
1552
+ catch {
1553
+ return false;
1554
+ }
1555
+ }
1556
+
1557
+ // POLICY: Every function/method in this file MUST be wrapped in try/catch.
1558
+ // The package must NEVER throw an exception to the consumer's code.
1559
+ /**
1560
+ * useCollaboration — React hook for Draft.js collaboration.
1561
+ *
1562
+ * Thin React wrapper over CollaborationManager from ./manager (Draft.js is a
1563
+ * React-only editor; this hook matches the useCollaboration convention of
1564
+ * the other Velt CRDT React wrappers).
1565
+ *
1566
+ * Draft.js is a controlled editor: the consumer owns the EditorState and
1567
+ * passes getEditorState/setEditorState accessors. Wire the returned
1568
+ * `handleChange` into the <Editor onChange> pipeline:
1569
+ *
1570
+ * const [editorState, setEditorState] = useState(EditorState.createEmpty());
1571
+ * const { handleChange } = useCollaboration({
1572
+ * editorId, getEditorState: () => editorState, setEditorState,
1573
+ * });
1574
+ * <Editor editorState={editorState} onChange={(s) => setEditorState(handleChange(s))} />
1575
+ */
1576
+ function useCollaboration(config) {
1577
+ const { client } = useVeltClient();
1578
+ const veltInitState = useVeltInitState();
1579
+ const [manager, setManager] = useState(null);
1580
+ const [isLoading, setIsLoading] = useState(true);
1581
+ const [synced, setSynced] = useState(false);
1582
+ const [status, setStatus] = useState('connecting');
1583
+ const [error, setError] = useState(null);
1584
+ const [versions, setVersions] = useState([]);
1585
+ const managerRef = useRef(null);
1586
+ // Tracks component mount across the whole lifecycle so the stable version
1587
+ // callbacks below never set state after unmount.
1588
+ const isMountedRef = useRef(true);
1589
+ useEffect(() => {
1590
+ isMountedRef.current = true;
1591
+ return () => {
1592
+ isMountedRef.current = false;
1593
+ };
1594
+ }, []);
1595
+ // Keep the consumer's controlled-state accessors fresh without
1596
+ // recreating the manager on every render.
1597
+ const getEditorStateRef = useRef(config.getEditorState);
1598
+ const setEditorStateRef = useRef(config.setEditorState);
1599
+ getEditorStateRef.current = config.getEditorState;
1600
+ setEditorStateRef.current = config.setEditorState;
1601
+ // Resolve veltClient: explicit config override or context fallback
1602
+ const veltClient = config.veltClient ?? client;
1603
+ useEffect(() => {
1604
+ try {
1605
+ // Gate 1: Velt must be initialized
1606
+ if (!veltClient || !veltInitState)
1607
+ return;
1608
+ // Gate 2: User must exist (avoid creating an anonymous store)
1609
+ let user;
1610
+ try {
1611
+ user = veltClient.getUser?.();
1612
+ }
1613
+ catch {
1614
+ // getUser may throw if Velt isn't ready
1615
+ }
1616
+ if (!user)
1617
+ return;
1618
+ // Gate 3: Controlled-state accessors must exist
1619
+ if (!config.getEditorState || !config.setEditorState)
1620
+ return;
1621
+ let mounted = true;
1622
+ const init = async () => {
1623
+ try {
1624
+ const created = await createCollaboration({
1625
+ ...config,
1626
+ veltClient,
1627
+ getEditorState: () => getEditorStateRef.current(),
1628
+ setEditorState: (state) => setEditorStateRef.current(state),
1629
+ // Bridge non-fatal manager errors into the hook's error state and
1630
+ // forward to the consumer's own handler.
1631
+ onError: (err) => {
1632
+ try {
1633
+ if (mounted)
1634
+ setError(err);
1635
+ config.onError?.(err);
1636
+ }
1637
+ catch { /* ignore */ }
1638
+ },
1639
+ });
1640
+ if (!mounted) {
1641
+ try {
1642
+ created.destroy();
1643
+ }
1644
+ catch { /* ignore */ }
1645
+ return;
1646
+ }
1647
+ managerRef.current = created;
1648
+ setManager(created);
1649
+ created.onStatusChange((s) => {
1650
+ try {
1651
+ if (mounted)
1652
+ setStatus(s);
1653
+ }
1654
+ catch { /* ignore */ }
1655
+ });
1656
+ created.onSynced((isSyncedNow) => {
1657
+ try {
1658
+ if (mounted)
1659
+ setSynced(isSyncedNow);
1660
+ }
1661
+ catch { /* ignore */ }
1662
+ });
1663
+ // Draft.js manager exposes an event-driven version-change signal —
1664
+ // the event carries metadata only (no list), so refetch on each one
1665
+ // to keep the reactive list live without polling.
1666
+ try {
1667
+ created.onVersionsChange(() => {
1668
+ try {
1669
+ void created.getVersions().then((list) => {
1670
+ try {
1671
+ if (mounted && isMountedRef.current)
1672
+ setVersions(list);
1673
+ }
1674
+ catch { /* ignore */ }
1675
+ }).catch((err) => {
1676
+ logError('Failed to refresh versions after change event', err);
1677
+ });
1678
+ }
1679
+ catch { /* ignore */ }
1680
+ });
1681
+ }
1682
+ catch (err) {
1683
+ logError('Failed to subscribe to version changes', err);
1684
+ }
1685
+ // Capture current state (fast-connection race condition)
1686
+ if (created.synced)
1687
+ setSynced(true);
1688
+ if (created.status !== 'connecting')
1689
+ setStatus(created.status);
1690
+ try {
1691
+ const list = await created.getVersions();
1692
+ if (mounted)
1693
+ setVersions(list);
1694
+ }
1695
+ catch (err) {
1696
+ logError('Failed to load initial versions', err);
1697
+ }
1698
+ if (mounted)
1699
+ setIsLoading(false);
1700
+ }
1701
+ catch (err) {
1702
+ logError('Failed to initialize collaboration', err);
1703
+ if (mounted) {
1704
+ setError(err instanceof Error ? err : new Error(String(err)));
1705
+ setIsLoading(false);
1706
+ }
1707
+ }
1708
+ };
1709
+ init();
1710
+ return () => {
1711
+ try {
1712
+ mounted = false;
1713
+ if (managerRef.current) {
1714
+ managerRef.current.destroy();
1715
+ managerRef.current = null;
1716
+ }
1717
+ setManager(null);
1718
+ setIsLoading(true);
1719
+ setSynced(false);
1720
+ setStatus('connecting');
1721
+ setError(null);
1722
+ setVersions([]);
1723
+ }
1724
+ catch { /* ignore cleanup errors */ }
1725
+ };
1726
+ }
1727
+ catch (err) {
1728
+ logError('Error in useCollaboration effect', err);
1729
+ }
1730
+ // Content/debounce changes don't recreate the manager (captured at init).
1731
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1732
+ }, [veltClient, veltInitState, config.editorId]);
1733
+ /** Route local Draft.js changes through the CRDT (identity while loading). */
1734
+ const handleChange = useCallback((nextState) => {
1735
+ try {
1736
+ const active = managerRef.current;
1737
+ if (!active)
1738
+ return nextState;
1739
+ return active.handleChange(nextState);
1740
+ }
1741
+ catch (err) {
1742
+ logError('Failed to handle editor change', err);
1743
+ return nextState;
1744
+ }
1745
+ }, []);
1746
+ // --- Version methods (stable callbacks) ---
1747
+ const refreshVersions = useCallback(async () => {
1748
+ try {
1749
+ const list = (await managerRef.current?.getVersions()) ?? [];
1750
+ if (isMountedRef.current)
1751
+ setVersions(list);
1752
+ return list;
1753
+ }
1754
+ catch (err) {
1755
+ logError('Failed to refresh versions', err);
1756
+ return [];
1757
+ }
1758
+ }, []);
1759
+ const saveVersion = useCallback(async (name) => {
1760
+ try {
1761
+ const id = (await managerRef.current?.saveVersion(name)) ?? '';
1762
+ try {
1763
+ const list = (await managerRef.current?.getVersions()) ?? [];
1764
+ if (isMountedRef.current)
1765
+ setVersions(list);
1766
+ }
1767
+ catch { /* list refresh is best-effort */ }
1768
+ return id;
1769
+ }
1770
+ catch (err) {
1771
+ logError('Failed to save version', err);
1772
+ return '';
1773
+ }
1774
+ }, []);
1775
+ const restoreVersion = useCallback(async (versionId) => {
1776
+ try {
1777
+ return (await managerRef.current?.restoreVersion(versionId)) ?? false;
1778
+ }
1779
+ catch (err) {
1780
+ logError('Failed to restore version', err);
1781
+ return false;
1782
+ }
1783
+ }, []);
1784
+ const destroy = useCallback(() => {
1785
+ try {
1786
+ managerRef.current?.destroy();
1787
+ managerRef.current = null;
1788
+ setManager(null);
1789
+ setSynced(false);
1790
+ setStatus('disconnected');
1791
+ }
1792
+ catch (err) {
1793
+ logError('Failed to destroy manager', err);
1794
+ }
1795
+ }, []);
1796
+ return {
1797
+ handleChange,
1798
+ manager,
1799
+ isLoading,
1800
+ synced,
1801
+ isSynced: synced,
1802
+ status,
1803
+ error,
1804
+ versions,
1805
+ saveVersion,
1806
+ restoreVersion,
1807
+ refreshVersions,
1808
+ destroy,
1809
+ };
1810
+ }
1811
+ /** Library-specific alias (mirrors useSlateCollaboration / useTinyMceCollaboration). */
1812
+ const useDraftJsCollaboration = useCollaboration;
1813
+
1814
+ export { CollaborationManager, createCollaboration, useCollaboration, useDraftJsCollaboration };
1815
+ //# sourceMappingURL=index.js.map