@veltdev/ace-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/cjs/index.js ADDED
@@ -0,0 +1,1610 @@
1
+ 'use strict';
2
+
3
+ var crdt = require('@veltdev/crdt');
4
+ var Y = require('yjs');
5
+
6
+ function _interopNamespaceDefault(e) {
7
+ var n = Object.create(null);
8
+ if (e) {
9
+ Object.keys(e).forEach(function (k) {
10
+ if (k !== 'default') {
11
+ var d = Object.getOwnPropertyDescriptor(e, k);
12
+ Object.defineProperty(n, k, d.get ? d : {
13
+ enumerable: true,
14
+ get: function () { return e[k]; }
15
+ });
16
+ }
17
+ });
18
+ }
19
+ n.default = e;
20
+ return Object.freeze(n);
21
+ }
22
+
23
+ var Y__namespace = /*#__PURE__*/_interopNamespaceDefault(Y);
24
+
25
+ // POLICY: Every function/method in this file MUST be wrapped in try/catch.
26
+ // The package must NEVER throw an exception to the consumer's code.
27
+ const LOG_PREFIX = '[VeltAceCRDT]';
28
+ /**
29
+ * Log an error/warning — only visible when debug mode is enabled
30
+ * via sessionStorage.setItem('forceCrdtDebugMode', 'true').
31
+ */
32
+ function logError(message, error) {
33
+ try {
34
+ if (typeof sessionStorage !== 'undefined' &&
35
+ sessionStorage.getItem('forceCrdtDebugMode')) {
36
+ console.warn(`${LOG_PREFIX} ${message}`, error !== undefined ? error : '');
37
+ }
38
+ }
39
+ catch {
40
+ // sessionStorage may not be available (SSR / iframe restrictions)
41
+ }
42
+ }
43
+
44
+ // POLICY: Every function/method in this file MUST be wrapped in try/catch.
45
+ // The package must NEVER throw an exception to the consumer's code.
46
+ const CONFIG_MAP_KEY = 'veltConfig';
47
+ const INITIALIZED_FLAG = '__docInitialized';
48
+ const LEGACY_FLAG = 'hasContentSet';
49
+ function normalizeText(value) {
50
+ try {
51
+ if (typeof value !== 'string')
52
+ return '';
53
+ return value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
54
+ }
55
+ catch (error) {
56
+ logError('Failed to normalize text', error);
57
+ return '';
58
+ }
59
+ }
60
+ function applyInitialContent(doc, ytext, initialContent, force) {
61
+ try {
62
+ const configMap = doc.getMap(CONFIG_MAP_KEY);
63
+ const text = normalizeText(initialContent);
64
+ if (!force) {
65
+ if (configMap.get(INITIALIZED_FLAG) || configMap.get(LEGACY_FLAG)) {
66
+ return false;
67
+ }
68
+ if (ytext.length > 0) {
69
+ configMap.set(INITIALIZED_FLAG, true);
70
+ return false;
71
+ }
72
+ }
73
+ doc.transact(() => {
74
+ if (ytext.length > 0)
75
+ ytext.delete(0, ytext.length);
76
+ if (text.length > 0)
77
+ ytext.insert(0, text);
78
+ configMap.set(INITIALIZED_FLAG, true);
79
+ }, 'velt-ace-initial-content');
80
+ return true;
81
+ }
82
+ catch (error) {
83
+ logError('Failed to apply initial content', error);
84
+ return false;
85
+ }
86
+ }
87
+ function replaceYText(doc, ytext, value, origin) {
88
+ try {
89
+ const text = normalizeText(value);
90
+ doc.transact(() => {
91
+ try {
92
+ if (ytext.length > 0)
93
+ ytext.delete(0, ytext.length);
94
+ if (text.length > 0)
95
+ ytext.insert(0, text);
96
+ }
97
+ catch (error) {
98
+ logError('Failed inside Y.Text replace transaction', error);
99
+ }
100
+ }, origin);
101
+ }
102
+ catch (error) {
103
+ logError('Failed to replace Y.Text', error);
104
+ }
105
+ }
106
+ function positionToIndex(text, position) {
107
+ try {
108
+ const normalized = normalizeText(text);
109
+ const lines = normalized.split('\n');
110
+ const row = Math.max(0, Math.min(Number(position.row) || 0, Math.max(0, lines.length - 1)));
111
+ const column = Math.max(0, Math.min(Number(position.column) || 0, lines[row]?.length ?? 0));
112
+ let index = 0;
113
+ for (let i = 0; i < row; i += 1) {
114
+ index += (lines[i]?.length ?? 0) + 1;
115
+ }
116
+ return Math.max(0, Math.min(index + column, normalized.length));
117
+ }
118
+ catch (error) {
119
+ logError('Failed to convert Ace position to index', error);
120
+ return 0;
121
+ }
122
+ }
123
+ function indexToPosition(text, index) {
124
+ try {
125
+ const normalized = normalizeText(text);
126
+ const bounded = Math.max(0, Math.min(Number(index) || 0, normalized.length));
127
+ const before = normalized.slice(0, bounded);
128
+ const lines = before.split('\n');
129
+ return {
130
+ row: Math.max(0, lines.length - 1),
131
+ column: lines[lines.length - 1]?.length ?? 0,
132
+ };
133
+ }
134
+ catch (error) {
135
+ logError('Failed to convert text index to Ace position', error);
136
+ return { row: 0, column: 0 };
137
+ }
138
+ }
139
+ function rangeToOffsets(text, range) {
140
+ try {
141
+ const anchor = positionToIndex(text, range.start);
142
+ const head = positionToIndex(text, range.end);
143
+ return { anchor, head };
144
+ }
145
+ catch (error) {
146
+ logError('Failed to convert Ace range to offsets', error);
147
+ return { anchor: 0, head: 0 };
148
+ }
149
+ }
150
+ function offsetsToRange(text, anchor, head) {
151
+ try {
152
+ return {
153
+ start: indexToPosition(text, Math.min(anchor, head)),
154
+ end: indexToPosition(text, Math.max(anchor, head)),
155
+ isBackwards: head < anchor,
156
+ };
157
+ }
158
+ catch (error) {
159
+ logError('Failed to convert offsets to Ace range', error);
160
+ return {
161
+ start: { row: 0, column: 0 },
162
+ end: { row: 0, column: 0 },
163
+ };
164
+ }
165
+ }
166
+
167
+ // POLICY: Every function/method in this file MUST be wrapped in try/catch.
168
+ // The package must NEVER throw an exception to the consumer's code.
169
+ const CONTENT_KEY = 'content';
170
+ const DEFAULT_EDITOR_ID = 'velt-ace-crdt-document';
171
+ const DEFAULT_CURSOR_COLOR = '#2563eb';
172
+ const REMOTE_MARKER_PREFIX = 'velt-ace-remote-marker';
173
+ const REMOTE_STYLE_ID = 'velt-ace-crdt-remote-marker-styles';
174
+ const LABEL_BELOW_CLASS = 'velt-ace-remote-marker-label-below';
175
+ class CollaborationManager {
176
+ constructor(config) {
177
+ this.store = null;
178
+ this.provider = null;
179
+ this.undoManager = null;
180
+ this.editor = null;
181
+ this.session = null;
182
+ this.rangeFactory = null;
183
+ this.userId = 'anonymous';
184
+ this._initialized = false;
185
+ this._synced = false;
186
+ this._status = 'connecting';
187
+ this._initialContentApplied = false;
188
+ this._lastFlushReason = null;
189
+ this.statusListeners = new Set();
190
+ this.syncedListeners = new Set();
191
+ this.awarenessListeners = new Set();
192
+ this.cleanupFns = [];
193
+ this.editorCleanupFns = [];
194
+ this.remoteMarkerIds = [];
195
+ this.remoteMarkerClasses = new Set();
196
+ this.selectionPublishTimer = null;
197
+ this.localSelectionSignature = null;
198
+ this.remoteRenderTimer = null;
199
+ this.isApplyingRemote = false;
200
+ this.isApplyingLocal = false;
201
+ try {
202
+ this.config = this.normalizeConfig(config);
203
+ this.editor = this.config.editor ?? null;
204
+ this.rangeFactory = this.config.rangeFactory ?? null;
205
+ }
206
+ catch (error) {
207
+ logError('Failed to construct CollaborationManager', error);
208
+ this.config = this.normalizeConfig({});
209
+ }
210
+ }
211
+ async initialize() {
212
+ try {
213
+ if (this._initialized)
214
+ return;
215
+ this.userId = this.readUserId();
216
+ this.store = this.createStore();
217
+ this.setupAwareness();
218
+ this.setupStoreTextObserver();
219
+ this.setupAwarenessListeners();
220
+ if (this.config.editor) {
221
+ this.attachEditor(this.config.editor, { destroyExisting: false, rangeFactory: this.config.rangeFactory });
222
+ }
223
+ try {
224
+ await this.store.initialize();
225
+ }
226
+ catch (error) {
227
+ if (this.isCorruptionError(error)) {
228
+ await this.tryRecoverFromCorruption();
229
+ }
230
+ else {
231
+ this.reportError('Store initialization failed', error);
232
+ }
233
+ }
234
+ this.provider = this.store.getProvider();
235
+ this.setupProviderListeners();
236
+ this.setupUndoManager();
237
+ this._initialized = true;
238
+ this.tryApplyInitialContent();
239
+ this.syncTextToEditor('initialize');
240
+ }
241
+ catch (error) {
242
+ this.reportError('Initialization failed unexpectedly', error);
243
+ }
244
+ }
245
+ destroy() {
246
+ try {
247
+ this.detachEditor();
248
+ for (const fn of this.cleanupFns.splice(0)) {
249
+ try {
250
+ fn();
251
+ }
252
+ catch { /* ignore cleanup */ }
253
+ }
254
+ try {
255
+ this.undoManager?.destroy();
256
+ }
257
+ catch { /* ignore */ }
258
+ this.undoManager = null;
259
+ try {
260
+ this.getAwareness()?.setLocalState(null);
261
+ }
262
+ catch { /* ignore */ }
263
+ try {
264
+ this.store?.destroy();
265
+ }
266
+ catch { /* ignore */ }
267
+ this.store = null;
268
+ this.provider = null;
269
+ this.statusListeners.clear();
270
+ this.syncedListeners.clear();
271
+ this.awarenessListeners.clear();
272
+ this._initialized = false;
273
+ this._synced = false;
274
+ this._status = 'disconnected';
275
+ }
276
+ catch (error) {
277
+ logError('Error during destroy', error);
278
+ }
279
+ }
280
+ attachEditor(editor, options = {}) {
281
+ try {
282
+ if (!editor)
283
+ return false;
284
+ if (options.destroyExisting !== false)
285
+ this.detachEditor();
286
+ this.editor = editor;
287
+ this.session = this.resolveSession(editor);
288
+ this.rangeFactory = options.rangeFactory ?? this.rangeFactory ?? this.config.rangeFactory ?? null;
289
+ this.bindEditorEvents();
290
+ this.syncTextToEditor('attach-editor');
291
+ this.publishCurrentSelection();
292
+ return true;
293
+ }
294
+ catch (error) {
295
+ this.reportError('Failed to attach Ace editor', error);
296
+ return false;
297
+ }
298
+ }
299
+ detachEditor() {
300
+ try {
301
+ this.clearSelectionPublishTimer();
302
+ this.clearRemoteRenderTimer();
303
+ this.clearRemoteMarkers();
304
+ for (const fn of this.editorCleanupFns.splice(0)) {
305
+ try {
306
+ fn();
307
+ }
308
+ catch { /* ignore cleanup */ }
309
+ }
310
+ this.editor = null;
311
+ this.session = null;
312
+ }
313
+ catch (error) {
314
+ logError('Failed to detach Ace editor', error);
315
+ }
316
+ }
317
+ get initialized() {
318
+ try {
319
+ return this._initialized;
320
+ }
321
+ catch {
322
+ return false;
323
+ }
324
+ }
325
+ get synced() {
326
+ try {
327
+ return this._synced;
328
+ }
329
+ catch {
330
+ return false;
331
+ }
332
+ }
333
+ get status() {
334
+ try {
335
+ return this._status;
336
+ }
337
+ catch {
338
+ return 'disconnected';
339
+ }
340
+ }
341
+ get editorAttached() {
342
+ try {
343
+ return !!this.editor;
344
+ }
345
+ catch {
346
+ return false;
347
+ }
348
+ }
349
+ getCollaborationPrimitives() {
350
+ try {
351
+ return {
352
+ ytext: this.getText(),
353
+ awareness: this.getAwareness(),
354
+ undoManager: this.undoManager,
355
+ doc: this.getDoc(),
356
+ provider: this.getProvider(),
357
+ };
358
+ }
359
+ catch (error) {
360
+ logError('Failed to get collaboration primitives', error);
361
+ return { ytext: null, awareness: null, undoManager: null, doc: null, provider: null };
362
+ }
363
+ }
364
+ getValue() {
365
+ try {
366
+ return this.getText()?.toString() ?? normalizeText(this.readEditorValue());
367
+ }
368
+ catch (error) {
369
+ logError('Failed to get value', error);
370
+ return '';
371
+ }
372
+ }
373
+ setValue(value, reason = 'set-value') {
374
+ try {
375
+ const ytext = this.getText();
376
+ const doc = this.getDoc();
377
+ if (!ytext || !doc)
378
+ return false;
379
+ replaceYText(doc, ytext, value, this);
380
+ this._lastFlushReason = reason;
381
+ this.syncTextToEditor(reason);
382
+ return true;
383
+ }
384
+ catch (error) {
385
+ this.reportError('Failed to set value', error);
386
+ return false;
387
+ }
388
+ }
389
+ insertText(value, index) {
390
+ try {
391
+ const ytext = this.getText();
392
+ const doc = this.getDoc();
393
+ if (!ytext || !doc)
394
+ return false;
395
+ const text = normalizeText(value);
396
+ const insertAt = Number.isFinite(Number(index))
397
+ ? Math.max(0, Math.min(Number(index), ytext.length))
398
+ : positionToIndex(ytext.toString(), this.readCursorPosition());
399
+ doc.transact(() => {
400
+ try {
401
+ if (text.length > 0)
402
+ ytext.insert(insertAt, text);
403
+ }
404
+ catch (error) {
405
+ logError('Failed inside insert transaction', error);
406
+ }
407
+ }, this);
408
+ this._lastFlushReason = 'insert-text';
409
+ this.syncTextToEditor('insert-text');
410
+ return true;
411
+ }
412
+ catch (error) {
413
+ this.reportError('Failed to insert text', error);
414
+ return false;
415
+ }
416
+ }
417
+ flushEditorToStore(reason = 'manual') {
418
+ try {
419
+ const ytext = this.getText();
420
+ const doc = this.getDoc();
421
+ if (!ytext || !doc)
422
+ return false;
423
+ const value = normalizeText(this.readEditorValue());
424
+ if (value !== ytext.toString()) {
425
+ replaceYText(doc, ytext, value, this);
426
+ }
427
+ this._lastFlushReason = reason;
428
+ return true;
429
+ }
430
+ catch (error) {
431
+ this.reportError('Failed to flush Ace editor to Store', error);
432
+ return false;
433
+ }
434
+ }
435
+ async forceReset(initialContent) {
436
+ try {
437
+ const ytext = this.getText();
438
+ const doc = this.getDoc();
439
+ if (!ytext || !doc)
440
+ return false;
441
+ const next = initialContent ?? this.config.initialContent ?? '';
442
+ replaceYText(doc, ytext, next, this);
443
+ this._lastFlushReason = 'force-reset';
444
+ this._initialContentApplied = true;
445
+ this.syncTextToEditor('force-reset');
446
+ return true;
447
+ }
448
+ catch (error) {
449
+ this.reportError('Failed to force reset Ace content', error);
450
+ return false;
451
+ }
452
+ }
453
+ onStatusChange(cb) {
454
+ try {
455
+ this.statusListeners.add(cb);
456
+ }
457
+ catch (error) {
458
+ logError('Failed to add status listener', error);
459
+ }
460
+ return () => {
461
+ try {
462
+ this.statusListeners.delete(cb);
463
+ }
464
+ catch { /* ignore */ }
465
+ };
466
+ }
467
+ onSynced(cb) {
468
+ try {
469
+ this.syncedListeners.add(cb);
470
+ }
471
+ catch (error) {
472
+ logError('Failed to add synced listener', error);
473
+ }
474
+ return () => {
475
+ try {
476
+ this.syncedListeners.delete(cb);
477
+ }
478
+ catch { /* ignore */ }
479
+ };
480
+ }
481
+ onRemoteSelectionsChange(cb) {
482
+ try {
483
+ this.awarenessListeners.add(cb);
484
+ cb(this.getRemoteSelections());
485
+ }
486
+ catch (error) {
487
+ logError('Failed to add remote selection listener', error);
488
+ }
489
+ return () => {
490
+ try {
491
+ this.awarenessListeners.delete(cb);
492
+ }
493
+ catch { /* ignore */ }
494
+ };
495
+ }
496
+ getDoc() {
497
+ try {
498
+ return this.store?.getDoc() ?? null;
499
+ }
500
+ catch (error) {
501
+ logError('Failed to get Y.Doc', error);
502
+ return null;
503
+ }
504
+ }
505
+ getText() {
506
+ try {
507
+ return this.store?.getText() ?? null;
508
+ }
509
+ catch (error) {
510
+ logError('Failed to get Y.Text', error);
511
+ return null;
512
+ }
513
+ }
514
+ getYText() {
515
+ try {
516
+ return this.getText();
517
+ }
518
+ catch (error) {
519
+ logError('Failed to get Y.Text alias', error);
520
+ return null;
521
+ }
522
+ }
523
+ getProvider() {
524
+ try {
525
+ return this.provider ?? this.store?.getProvider() ?? null;
526
+ }
527
+ catch (error) {
528
+ logError('Failed to get SyncProvider', error);
529
+ return null;
530
+ }
531
+ }
532
+ getAwareness() {
533
+ try {
534
+ return this.store?.getAwareness() ?? null;
535
+ }
536
+ catch (error) {
537
+ logError('Failed to get Awareness', error);
538
+ return null;
539
+ }
540
+ }
541
+ getUndoManager() {
542
+ try {
543
+ return this.undoManager;
544
+ }
545
+ catch (error) {
546
+ logError('Failed to get UndoManager', error);
547
+ return null;
548
+ }
549
+ }
550
+ getEditor() {
551
+ try {
552
+ return this.editor;
553
+ }
554
+ catch (error) {
555
+ logError('Failed to get Ace editor', error);
556
+ return null;
557
+ }
558
+ }
559
+ getStore() {
560
+ try {
561
+ return this.store;
562
+ }
563
+ catch (error) {
564
+ logError('Failed to get Store', error);
565
+ return null;
566
+ }
567
+ }
568
+ getStats() {
569
+ try {
570
+ return {
571
+ initialized: this.initialized,
572
+ synced: this.synced,
573
+ status: this.status,
574
+ hasEditor: !!this.editor,
575
+ hasStore: !!this.store,
576
+ remoteSelectionCount: this.getRemoteSelections().length,
577
+ textLength: this.getValue().length,
578
+ lastFlushReason: this._lastFlushReason,
579
+ };
580
+ }
581
+ catch {
582
+ return {
583
+ initialized: false,
584
+ synced: false,
585
+ status: 'disconnected',
586
+ hasEditor: false,
587
+ hasStore: false,
588
+ remoteSelectionCount: 0,
589
+ textLength: 0,
590
+ lastFlushReason: null,
591
+ };
592
+ }
593
+ }
594
+ async saveVersion(name) {
595
+ try {
596
+ this.flushEditorToStore('save-version');
597
+ return (await this.store?.saveVersion(name)) ?? '';
598
+ }
599
+ catch (error) {
600
+ logError('Failed to save version', error);
601
+ return '';
602
+ }
603
+ }
604
+ async getVersions() {
605
+ try {
606
+ return (await this.store?.getVersions()) ?? [];
607
+ }
608
+ catch (error) {
609
+ logError('Failed to get versions', error);
610
+ return [];
611
+ }
612
+ }
613
+ async getVersionById(versionId) {
614
+ try {
615
+ return (await this.store?.getVersionById(versionId)) ?? null;
616
+ }
617
+ catch (error) {
618
+ logError('Failed to get version by id', error);
619
+ return null;
620
+ }
621
+ }
622
+ async restoreVersion(versionId) {
623
+ try {
624
+ const versions = (await this.store?.getVersions()) ?? [];
625
+ const hasVersion = versions.some((version) => version?.versionId === versionId || version?.id === versionId);
626
+ if (!hasVersion)
627
+ return false;
628
+ const restored = (await this.store?.restoreVersion(versionId)) ?? false;
629
+ if (restored) {
630
+ this._lastFlushReason = 'restore-version';
631
+ this.syncTextToEditor('restore-version');
632
+ }
633
+ return restored;
634
+ }
635
+ catch (error) {
636
+ logError('Failed to restore version', error);
637
+ return false;
638
+ }
639
+ }
640
+ async setStateFromVersion(version) {
641
+ try {
642
+ await this.store?.setStateFromVersion(version);
643
+ this._lastFlushReason = 'set-state-from-version';
644
+ this.syncTextToEditor('set-state-from-version');
645
+ }
646
+ catch (error) {
647
+ logError('Failed to set state from version', error);
648
+ }
649
+ }
650
+ setLocalSelection(selection) {
651
+ try {
652
+ if (!this.store)
653
+ return false;
654
+ if (!selection) {
655
+ if (this.localSelectionSignature === null)
656
+ return true;
657
+ this.localSelectionSignature = null;
658
+ this.store.updateAwareness?.({ selection: null });
659
+ this.notifyAwarenessListeners();
660
+ return true;
661
+ }
662
+ const normalized = this.createAwarenessSelection(selection);
663
+ if (!normalized)
664
+ return false;
665
+ const signature = this.createSelectionSignature(normalized);
666
+ if (signature === this.localSelectionSignature)
667
+ return true;
668
+ this.localSelectionSignature = signature;
669
+ this.store.updateAwareness?.({ selection: normalized });
670
+ this.notifyAwarenessListeners();
671
+ return true;
672
+ }
673
+ catch (error) {
674
+ this.reportError('Failed to set local selection', error);
675
+ return false;
676
+ }
677
+ }
678
+ publishCurrentSelection(kind = 'cursor') {
679
+ try {
680
+ const range = this.readSelectionRange();
681
+ if (!range)
682
+ return false;
683
+ const collapsed = this.isCollapsed(range);
684
+ const effectiveKind = kind !== 'highlight' && collapsed
685
+ ? 'cursor'
686
+ : kind === 'cursor' && !collapsed ? 'selection' : kind;
687
+ return this.setLocalSelection({ ...range, kind: effectiveKind, text: this.readSelectedText() });
688
+ }
689
+ catch (error) {
690
+ this.reportError('Failed to publish current selection', error);
691
+ return false;
692
+ }
693
+ }
694
+ addHighlight(range, color) {
695
+ try {
696
+ const current = range?.start && range?.end ? range : this.readSelectionRange();
697
+ if (!current)
698
+ return false;
699
+ const highlightRange = this.expandCollapsedHighlightRange(current);
700
+ return this.setLocalSelection({
701
+ ...highlightRange,
702
+ kind: 'highlight',
703
+ color: color ?? this.resolveCursorData().color,
704
+ colorLight: this.withAlpha(color ?? this.resolveCursorData().color, '33'),
705
+ text: this.readSelectedText(),
706
+ });
707
+ }
708
+ catch (error) {
709
+ this.reportError('Failed to add highlight marker', error);
710
+ return false;
711
+ }
712
+ }
713
+ getRemoteSelections() {
714
+ try {
715
+ const awareness = this.getAwareness();
716
+ const doc = this.getDoc();
717
+ if (!awareness)
718
+ return [];
719
+ const localAwarenessClientId = Number(awareness.clientID);
720
+ const localDocClientId = doc?.clientID;
721
+ const selections = [];
722
+ awareness.getStates().forEach((state, clientId) => {
723
+ try {
724
+ if (clientId === localDocClientId || clientId === localAwarenessClientId)
725
+ return;
726
+ const selection = state?.selection;
727
+ if (!selection?.start || !selection?.end)
728
+ return;
729
+ selections.push({
730
+ ...selection,
731
+ userId: selection.userId ?? state.user?.userId,
732
+ name: selection.name ?? state.user?.name,
733
+ email: selection.email ?? state.user?.email,
734
+ color: selection.color ?? state.user?.color,
735
+ colorLight: selection.colorLight ?? state.user?.colorLight,
736
+ clientId,
737
+ });
738
+ }
739
+ catch {
740
+ // Ignore one malformed remote awareness state.
741
+ }
742
+ });
743
+ return selections;
744
+ }
745
+ catch (error) {
746
+ logError('Failed to get remote selections', error);
747
+ return [];
748
+ }
749
+ }
750
+ renderRemoteSelections() {
751
+ try {
752
+ const session = this.session;
753
+ if (!session?.addMarker)
754
+ return 0;
755
+ this.clearRemoteMarkers();
756
+ this.ensureRemoteMarkerStyles(this.getRemoteSelections());
757
+ let rendered = 0;
758
+ for (const selection of this.getRemoteSelections()) {
759
+ try {
760
+ const range = this.createAceRange(selection.start, selection.end);
761
+ const className = this.createMarkerClassName(selection);
762
+ // The name label renders above the marker line; on the first row it
763
+ // would be clipped by the scroller, so flip it below the line there.
764
+ const labelModifier = this.normalizePosition(selection.start).row === 0
765
+ ? ` ${LABEL_BELOW_CLASS}`
766
+ : '';
767
+ // inFront=true — Ace paints back markers BEHIND the text layer, which
768
+ // hides the caret border and the ::after name label under the glyphs.
769
+ const markerId = session.addMarker(range, className + labelModifier, 'text', true);
770
+ this.remoteMarkerIds.push(markerId);
771
+ rendered += 1;
772
+ }
773
+ catch {
774
+ // Ignore one bad remote marker.
775
+ }
776
+ }
777
+ return rendered;
778
+ }
779
+ catch (error) {
780
+ logError('Failed to render remote selections', error);
781
+ return 0;
782
+ }
783
+ }
784
+ clearRemoteMarkers() {
785
+ try {
786
+ const session = this.session;
787
+ if (!session?.removeMarker) {
788
+ this.remoteMarkerIds = [];
789
+ return;
790
+ }
791
+ for (const markerId of this.remoteMarkerIds.splice(0)) {
792
+ try {
793
+ session.removeMarker(markerId);
794
+ }
795
+ catch { /* ignore one marker */ }
796
+ }
797
+ }
798
+ catch (error) {
799
+ logError('Failed to clear remote markers', error);
800
+ }
801
+ }
802
+ normalizeConfig(config) {
803
+ try {
804
+ const input = (config ?? {});
805
+ return {
806
+ editorId: input.editorId || DEFAULT_EDITOR_ID,
807
+ veltClient: input.veltClient,
808
+ editor: input.editor ?? null,
809
+ initialContent: typeof input.initialContent === 'string' ? input.initialContent : undefined,
810
+ cursorData: input.cursorData,
811
+ rangeFactory: input.rangeFactory,
812
+ debounceMs: input.debounceMs,
813
+ onError: input.onError,
814
+ forceResetInitialContent: Boolean(input.forceResetInitialContent),
815
+ };
816
+ }
817
+ catch (error) {
818
+ logError('Failed to normalize config', error);
819
+ return {
820
+ editorId: DEFAULT_EDITOR_ID,
821
+ veltClient: undefined,
822
+ };
823
+ }
824
+ }
825
+ createStore() {
826
+ try {
827
+ return new crdt.Store({
828
+ id: this.config.editorId,
829
+ userId: this.userId,
830
+ type: 'text',
831
+ contentKey: CONTENT_KEY,
832
+ source: 'ace',
833
+ debounceMs: this.config.debounceMs,
834
+ veltClient: this.config.veltClient,
835
+ forceResetInitialContent: this.config.forceResetInitialContent,
836
+ initialValue: this.config.forceResetInitialContent ? '' : undefined,
837
+ });
838
+ }
839
+ catch (error) {
840
+ logError('Failed to create Store', error);
841
+ throw error;
842
+ }
843
+ }
844
+ setupStoreTextObserver() {
845
+ try {
846
+ const ytext = this.store?.getText();
847
+ if (!ytext)
848
+ return;
849
+ const observer = (event) => {
850
+ try {
851
+ if (event.transaction.origin === this || this.isApplyingLocal)
852
+ return;
853
+ this.syncTextToEditor('remote', event);
854
+ }
855
+ catch (error) {
856
+ logError('Y.Text observer failed', error);
857
+ }
858
+ };
859
+ ytext.observe(observer);
860
+ this.cleanupFns.push(() => {
861
+ try {
862
+ ytext.unobserve(observer);
863
+ }
864
+ catch { /* ignore */ }
865
+ });
866
+ }
867
+ catch (error) {
868
+ logError('Failed to setup Y.Text observer', error);
869
+ }
870
+ }
871
+ setupUndoManager() {
872
+ try {
873
+ const ytext = this.getText();
874
+ if (!ytext || this.undoManager)
875
+ return;
876
+ this.undoManager = new Y__namespace.UndoManager(ytext, {
877
+ trackedOrigins: new Set([this, null]),
878
+ });
879
+ }
880
+ catch (error) {
881
+ logError('Failed to setup UndoManager', error);
882
+ }
883
+ }
884
+ setupAwareness() {
885
+ try {
886
+ const cursorData = this.resolveCursorData();
887
+ this.store?.updateAwareness?.({
888
+ selection: undefined,
889
+ user: {
890
+ userId: this.userId,
891
+ name: cursorData.name || 'Anonymous',
892
+ email: cursorData.email || '',
893
+ color: cursorData.color || DEFAULT_CURSOR_COLOR,
894
+ colorLight: cursorData.colorLight || this.withAlpha(cursorData.color || DEFAULT_CURSOR_COLOR, '33'),
895
+ },
896
+ });
897
+ }
898
+ catch (error) {
899
+ logError('Failed to setup awareness', error);
900
+ }
901
+ }
902
+ setupProviderListeners() {
903
+ try {
904
+ if (!this.provider)
905
+ return;
906
+ const onSynced = (event) => {
907
+ try {
908
+ const synced = typeof event === 'boolean' ? event : Boolean(event?.synced);
909
+ this._synced = synced;
910
+ this.syncedListeners.forEach((cb) => {
911
+ try {
912
+ cb(synced);
913
+ }
914
+ catch { /* ignore listener errors */ }
915
+ });
916
+ if (synced) {
917
+ this.tryApplyInitialContent();
918
+ this.syncTextToEditor('synced');
919
+ }
920
+ }
921
+ catch (error) {
922
+ logError('Error in synced handler', error);
923
+ }
924
+ };
925
+ const onStatus = (event) => {
926
+ try {
927
+ const status = typeof event === 'string' ? event : event?.status;
928
+ if (!status)
929
+ return;
930
+ this._status = status;
931
+ this.statusListeners.forEach((cb) => {
932
+ try {
933
+ cb(status);
934
+ }
935
+ catch { /* ignore listener errors */ }
936
+ });
937
+ }
938
+ catch (error) {
939
+ logError('Error in status handler', error);
940
+ }
941
+ };
942
+ this.provider.on('synced', onSynced);
943
+ this.provider.on('sync', onSynced);
944
+ this.provider.on('status', onStatus);
945
+ this.cleanupFns.push(() => {
946
+ try {
947
+ this.provider?.off?.('synced', onSynced);
948
+ }
949
+ catch { /* ignore */ }
950
+ try {
951
+ this.provider?.off?.('sync', onSynced);
952
+ }
953
+ catch { /* ignore */ }
954
+ try {
955
+ this.provider?.off?.('status', onStatus);
956
+ }
957
+ catch { /* ignore */ }
958
+ });
959
+ const providerAny = this.provider;
960
+ if (typeof providerAny.synced === 'boolean')
961
+ this._synced = providerAny.synced;
962
+ if (providerAny.status)
963
+ this._status = providerAny.status;
964
+ }
965
+ catch (error) {
966
+ logError('Failed to setup provider listeners', error);
967
+ }
968
+ }
969
+ setupAwarenessListeners() {
970
+ try {
971
+ const awareness = this.store?.getAwareness();
972
+ if (!awareness?.on)
973
+ return;
974
+ const handler = () => {
975
+ try {
976
+ this.notifyAwarenessListeners();
977
+ this.scheduleRemoteSelectionRender();
978
+ }
979
+ catch (error) {
980
+ logError('Awareness handler failed', error);
981
+ }
982
+ };
983
+ awareness.on('change', handler);
984
+ awareness.on('update', handler);
985
+ this.cleanupFns.push(() => {
986
+ try {
987
+ awareness.off?.('change', handler);
988
+ }
989
+ catch { /* ignore */ }
990
+ try {
991
+ awareness.off?.('update', handler);
992
+ }
993
+ catch { /* ignore */ }
994
+ });
995
+ }
996
+ catch (error) {
997
+ logError('Failed to setup awareness listeners', error);
998
+ }
999
+ }
1000
+ bindEditorEvents() {
1001
+ try {
1002
+ const session = this.session;
1003
+ if (!session?.on)
1004
+ return;
1005
+ const onChange = (delta) => {
1006
+ try {
1007
+ this.applyAceDelta(delta);
1008
+ }
1009
+ catch (error) {
1010
+ logError('Ace change handler failed', error);
1011
+ }
1012
+ };
1013
+ session.on('change', onChange);
1014
+ this.editorCleanupFns.push(() => {
1015
+ try {
1016
+ session.off?.('change', onChange);
1017
+ }
1018
+ catch { /* ignore */ }
1019
+ });
1020
+ const selection = session.selection ?? this.editor?.selection;
1021
+ const onSelection = () => {
1022
+ try {
1023
+ this.scheduleSelectionPublish('selection', 90);
1024
+ }
1025
+ catch (error) {
1026
+ logError('Ace selection handler failed', error);
1027
+ }
1028
+ };
1029
+ const onCursor = () => {
1030
+ try {
1031
+ this.scheduleSelectionPublish('cursor', 160);
1032
+ }
1033
+ catch (error) {
1034
+ logError('Ace cursor handler failed', error);
1035
+ }
1036
+ };
1037
+ selection?.on?.('changeSelection', onSelection);
1038
+ selection?.on?.('changeCursor', onCursor);
1039
+ this.editorCleanupFns.push(() => {
1040
+ try {
1041
+ selection?.off?.('changeSelection', onSelection);
1042
+ }
1043
+ catch { /* ignore */ }
1044
+ try {
1045
+ selection?.off?.('changeCursor', onCursor);
1046
+ }
1047
+ catch { /* ignore */ }
1048
+ });
1049
+ }
1050
+ catch (error) {
1051
+ logError('Failed to bind Ace editor events', error);
1052
+ }
1053
+ }
1054
+ tryApplyInitialContent() {
1055
+ try {
1056
+ if (this._initialContentApplied)
1057
+ return;
1058
+ if (!this.store || this.config.initialContent === undefined)
1059
+ return;
1060
+ if (!this._synced && !this.config.forceResetInitialContent)
1061
+ return;
1062
+ const ytext = this.store.getText();
1063
+ if (!ytext)
1064
+ return;
1065
+ this._initialContentApplied = applyInitialContent(this.store.getDoc(), ytext, this.config.initialContent, this.config.forceResetInitialContent);
1066
+ if (this._initialContentApplied)
1067
+ this.syncTextToEditor('initial-content');
1068
+ }
1069
+ catch (error) {
1070
+ logError('Failed to apply initial content', error);
1071
+ }
1072
+ }
1073
+ applyAceDelta(delta) {
1074
+ try {
1075
+ if (this.isApplyingRemote || !delta?.action)
1076
+ return;
1077
+ const ytext = this.getText();
1078
+ const doc = this.getDoc();
1079
+ if (!ytext || !doc)
1080
+ return;
1081
+ const current = ytext.toString();
1082
+ const start = positionToIndex(current, delta.start ?? { row: 0, column: 0 });
1083
+ const end = positionToIndex(current, delta.end ?? delta.start ?? { row: 0, column: 0 });
1084
+ const payload = Array.isArray(delta.lines) ? delta.lines.join('\n') : normalizeText(delta.text ?? '');
1085
+ this.isApplyingLocal = true;
1086
+ doc.transact(() => {
1087
+ try {
1088
+ if (delta.action === 'insert') {
1089
+ if (payload.length > 0)
1090
+ ytext.insert(start, payload);
1091
+ }
1092
+ else if (delta.action === 'remove') {
1093
+ const length = Math.max(0, end - start);
1094
+ if (length > 0)
1095
+ ytext.delete(start, length);
1096
+ }
1097
+ else {
1098
+ replaceYText(doc, ytext, this.readEditorValue(), this);
1099
+ }
1100
+ }
1101
+ catch (error) {
1102
+ logError('Failed inside Ace delta transaction', error);
1103
+ }
1104
+ }, this);
1105
+ this._lastFlushReason = 'local-edit';
1106
+ }
1107
+ catch (error) {
1108
+ this.reportError('Failed to apply Ace delta', error);
1109
+ }
1110
+ finally {
1111
+ this.isApplyingLocal = false;
1112
+ }
1113
+ }
1114
+ syncTextToEditor(reason, event) {
1115
+ try {
1116
+ const ytext = this.getText();
1117
+ const editor = this.editor;
1118
+ const session = this.session ?? this.resolveSession(editor);
1119
+ if (!ytext || !editor || !session)
1120
+ return;
1121
+ const next = ytext.toString();
1122
+ const current = normalizeText(this.readEditorValue());
1123
+ if (current === next)
1124
+ return;
1125
+ this.isApplyingRemote = true;
1126
+ try {
1127
+ const appliedIncrementally = event ? this.applyRemoteDeltaToSession(session, event) : false;
1128
+ if (!appliedIncrementally || normalizeText(this.readEditorValue()) !== next) {
1129
+ this.replaceEditorValuePreservingSelection(editor, session, next);
1130
+ }
1131
+ this._lastFlushReason = reason;
1132
+ }
1133
+ finally {
1134
+ this.isApplyingRemote = false;
1135
+ }
1136
+ }
1137
+ catch (error) {
1138
+ this.isApplyingRemote = false;
1139
+ this.reportError('Failed to sync Y.Text to Ace editor', error);
1140
+ }
1141
+ }
1142
+ // Applies a remote Y.Text delta as incremental session edits so Ace keeps
1143
+ // the local user's cursor/selection anchors instead of resetting them on a
1144
+ // whole-document setValue (which Ace treats as replace-all).
1145
+ applyRemoteDeltaToSession(session, event) {
1146
+ try {
1147
+ if (!session.insert || !session.remove)
1148
+ return false;
1149
+ const delta = event?.delta;
1150
+ if (!Array.isArray(delta) || delta.length === 0)
1151
+ return false;
1152
+ // Y.Text with plain strings only ever emits retain/insert/delete string ops;
1153
+ // bail out to full replacement on anything else (e.g. embeds).
1154
+ if (!delta.every((op) => typeof op.retain === 'number'
1155
+ || typeof op.insert === 'string'
1156
+ || typeof op.delete === 'number')) {
1157
+ return false;
1158
+ }
1159
+ let index = 0;
1160
+ for (const op of delta) {
1161
+ const currentText = normalizeText(session.getValue?.() ?? this.readEditorValue());
1162
+ if (typeof op.retain === 'number') {
1163
+ index += op.retain;
1164
+ }
1165
+ else if (typeof op.insert === 'string') {
1166
+ session.insert(indexToPosition(currentText, index), op.insert);
1167
+ index += op.insert.length;
1168
+ }
1169
+ else if (typeof op.delete === 'number') {
1170
+ const start = indexToPosition(currentText, index);
1171
+ const end = indexToPosition(currentText, index + op.delete);
1172
+ session.remove(this.createAceRange(start, end));
1173
+ }
1174
+ }
1175
+ return true;
1176
+ }
1177
+ catch (error) {
1178
+ logError('Failed to apply remote delta incrementally', error);
1179
+ return false;
1180
+ }
1181
+ }
1182
+ replaceEditorValuePreservingSelection(editor, session, next) {
1183
+ try {
1184
+ const currentText = normalizeText(this.readEditorValue());
1185
+ const selectionRange = this.readSelectionRange();
1186
+ const offsets = selectionRange ? rangeToOffsets(currentText, selectionRange) : null;
1187
+ if (session.setValue) {
1188
+ session.setValue(next);
1189
+ }
1190
+ else {
1191
+ editor.setValue?.(next, -1);
1192
+ }
1193
+ if (!offsets)
1194
+ return;
1195
+ const anchor = Math.min(offsets.anchor, next.length);
1196
+ const head = Math.min(offsets.head, next.length);
1197
+ const restored = offsetsToRange(next, anchor, head);
1198
+ const selectionApi = session.selection ?? editor.selection;
1199
+ if (anchor === head) {
1200
+ selectionApi?.moveToPosition?.(restored.end);
1201
+ }
1202
+ else {
1203
+ selectionApi?.setSelectionRange?.(this.createAceRange(restored.start, restored.end), restored.isBackwards);
1204
+ }
1205
+ }
1206
+ catch (error) {
1207
+ logError('Failed to replace Ace editor value', error);
1208
+ }
1209
+ }
1210
+ readEditorValue() {
1211
+ try {
1212
+ return normalizeText(this.editor?.getValue?.() ?? this.session?.getValue?.() ?? '');
1213
+ }
1214
+ catch (error) {
1215
+ logError('Failed to read Ace editor value', error);
1216
+ return '';
1217
+ }
1218
+ }
1219
+ readCursorPosition() {
1220
+ try {
1221
+ return this.session?.selection?.getCursor?.()
1222
+ ?? this.editor?.selection?.getCursor?.()
1223
+ ?? { row: 0, column: 0 };
1224
+ }
1225
+ catch {
1226
+ return { row: 0, column: 0 };
1227
+ }
1228
+ }
1229
+ readSelectionRange() {
1230
+ try {
1231
+ return this.editor?.getSelectionRange?.()
1232
+ ?? this.session?.selection?.getRange?.()
1233
+ ?? this.editor?.selection?.getRange?.()
1234
+ ?? null;
1235
+ }
1236
+ catch (error) {
1237
+ logError('Failed to read Ace selection range', error);
1238
+ return null;
1239
+ }
1240
+ }
1241
+ readSelectedText() {
1242
+ try {
1243
+ const range = this.readSelectionRange();
1244
+ if (this.editor?.getSelectedText)
1245
+ return normalizeText(this.editor.getSelectedText());
1246
+ if (range && this.session?.getTextRange)
1247
+ return normalizeText(this.session.getTextRange(range));
1248
+ return '';
1249
+ }
1250
+ catch {
1251
+ return '';
1252
+ }
1253
+ }
1254
+ resolveSession(editor) {
1255
+ try {
1256
+ return editor?.getSession?.() ?? editor?.session ?? null;
1257
+ }
1258
+ catch (error) {
1259
+ logError('Failed to resolve Ace session', error);
1260
+ return null;
1261
+ }
1262
+ }
1263
+ createAwarenessSelection(selection) {
1264
+ try {
1265
+ if (!selection.start || !selection.end)
1266
+ return null;
1267
+ const cursorData = this.resolveCursorData();
1268
+ return {
1269
+ start: this.normalizePosition(selection.start),
1270
+ end: this.normalizePosition(selection.end),
1271
+ isBackwards: Boolean(selection.isBackwards),
1272
+ userId: selection.userId ?? this.userId,
1273
+ name: selection.name ?? cursorData.name,
1274
+ email: selection.email ?? cursorData.email,
1275
+ color: selection.color ?? cursorData.color,
1276
+ colorLight: selection.colorLight ?? cursorData.colorLight,
1277
+ text: selection.text,
1278
+ kind: selection.kind ?? (this.isCollapsed(selection) ? 'cursor' : 'selection'),
1279
+ updatedAt: Date.now(),
1280
+ };
1281
+ }
1282
+ catch (error) {
1283
+ logError('Failed to create awareness selection', error);
1284
+ return null;
1285
+ }
1286
+ }
1287
+ notifyAwarenessListeners() {
1288
+ try {
1289
+ const selections = this.getRemoteSelections();
1290
+ for (const cb of this.awarenessListeners) {
1291
+ try {
1292
+ cb(selections);
1293
+ }
1294
+ catch { /* ignore listener errors */ }
1295
+ }
1296
+ }
1297
+ catch (error) {
1298
+ logError('Failed to notify awareness listeners', error);
1299
+ }
1300
+ }
1301
+ scheduleSelectionPublish(kind = 'cursor', delayMs = 120) {
1302
+ try {
1303
+ this.clearSelectionPublishTimer();
1304
+ const safeDelay = Math.max(0, Number(delayMs) || 0);
1305
+ this.selectionPublishTimer = setTimeout(() => {
1306
+ try {
1307
+ this.selectionPublishTimer = null;
1308
+ this.publishCurrentSelection(kind);
1309
+ }
1310
+ catch (error) {
1311
+ logError('Scheduled selection publish failed', error);
1312
+ }
1313
+ }, safeDelay);
1314
+ }
1315
+ catch (error) {
1316
+ logError('Failed to schedule selection publish', error);
1317
+ }
1318
+ }
1319
+ clearSelectionPublishTimer() {
1320
+ try {
1321
+ if (this.selectionPublishTimer)
1322
+ clearTimeout(this.selectionPublishTimer);
1323
+ this.selectionPublishTimer = null;
1324
+ }
1325
+ catch (error) {
1326
+ logError('Failed to clear selection publish timer', error);
1327
+ }
1328
+ }
1329
+ scheduleRemoteSelectionRender() {
1330
+ try {
1331
+ if (this.remoteRenderTimer)
1332
+ return;
1333
+ this.remoteRenderTimer = setTimeout(() => {
1334
+ try {
1335
+ this.remoteRenderTimer = null;
1336
+ this.renderRemoteSelections();
1337
+ }
1338
+ catch (error) {
1339
+ logError('Scheduled remote selection render failed', error);
1340
+ }
1341
+ }, 16);
1342
+ }
1343
+ catch (error) {
1344
+ logError('Failed to schedule remote selection render', error);
1345
+ }
1346
+ }
1347
+ clearRemoteRenderTimer() {
1348
+ try {
1349
+ if (this.remoteRenderTimer)
1350
+ clearTimeout(this.remoteRenderTimer);
1351
+ this.remoteRenderTimer = null;
1352
+ }
1353
+ catch (error) {
1354
+ logError('Failed to clear remote render timer', error);
1355
+ }
1356
+ }
1357
+ createAceRange(start, end) {
1358
+ try {
1359
+ const safeStart = this.normalizePosition(start);
1360
+ const safeEnd = this.normalizePosition(end);
1361
+ if (this.rangeFactory)
1362
+ return this.rangeFactory(safeStart, safeEnd);
1363
+ const globalAce = globalThis.ace;
1364
+ const Range = globalAce?.require?.('ace/range')?.Range;
1365
+ if (Range)
1366
+ return new Range(safeStart.row, safeStart.column, safeEnd.row, safeEnd.column);
1367
+ return { start: safeStart, end: safeEnd };
1368
+ }
1369
+ catch (error) {
1370
+ logError('Failed to create Ace range', error);
1371
+ return { start, end };
1372
+ }
1373
+ }
1374
+ ensureRemoteMarkerStyles(selections) {
1375
+ try {
1376
+ if (typeof document === 'undefined')
1377
+ return;
1378
+ let style = document.getElementById(REMOTE_STYLE_ID);
1379
+ if (!style) {
1380
+ style = document.createElement('style');
1381
+ style.id = REMOTE_STYLE_ID;
1382
+ // [class] bumps specificity above the per-client ::after rules below.
1383
+ style.appendChild(document.createTextNode(`.${LABEL_BELOW_CLASS}[class]::after{top:100%;bottom:auto;}`));
1384
+ document.head.appendChild(style);
1385
+ }
1386
+ const rules = [];
1387
+ for (const selection of selections) {
1388
+ try {
1389
+ const className = this.createMarkerClassName(selection);
1390
+ if (this.remoteMarkerClasses.has(className))
1391
+ continue;
1392
+ this.remoteMarkerClasses.add(className);
1393
+ const color = this.escapeCssColor(selection.color ?? DEFAULT_CURSOR_COLOR);
1394
+ const light = this.escapeCssColor(selection.colorLight ?? this.withAlpha(color, '33'));
1395
+ const label = this.escapeCssString(selection.name ?? selection.email ?? 'Remote');
1396
+ const kind = selection.kind ?? 'selection';
1397
+ const border = kind === 'cursor' ? `border-left:2px solid ${color};` : `border:1px solid ${color};border-left:2px solid ${color};`;
1398
+ const zIndex = kind === 'cursor' ? 9 : 8;
1399
+ rules.push(`.${className}{position:absolute;background:${kind === 'cursor' ? 'transparent' : light};${border}box-sizing:border-box;z-index:${zIndex};pointer-events:none;}`);
1400
+ rules.push(`.${className}::after{content:'${label}';position:absolute;top:-18px;left:0;background:${color};color:#fff;font:600 11px system-ui,sans-serif;padding:1px 5px;border-radius:3px;white-space:nowrap;}`);
1401
+ }
1402
+ catch {
1403
+ // Ignore one style rule.
1404
+ }
1405
+ }
1406
+ if (rules.length > 0)
1407
+ style.appendChild(document.createTextNode(rules.join('\n')));
1408
+ }
1409
+ catch (error) {
1410
+ logError('Failed to ensure remote marker styles', error);
1411
+ }
1412
+ }
1413
+ createMarkerClassName(selection) {
1414
+ try {
1415
+ const kind = String(selection.kind ?? 'selection').replace(/[^a-zA-Z0-9_-]/g, '');
1416
+ const clientId = String(selection.clientId).replace(/[^a-zA-Z0-9_-]/g, '');
1417
+ return `${REMOTE_MARKER_PREFIX}-${kind}-${clientId}`;
1418
+ }
1419
+ catch {
1420
+ return `${REMOTE_MARKER_PREFIX}-unknown`;
1421
+ }
1422
+ }
1423
+ createSelectionSignature(selection) {
1424
+ try {
1425
+ return [
1426
+ selection.kind ?? '',
1427
+ selection.start.row,
1428
+ selection.start.column,
1429
+ selection.end.row,
1430
+ selection.end.column,
1431
+ selection.isBackwards ? '1' : '0',
1432
+ selection.text ?? '',
1433
+ selection.color ?? '',
1434
+ selection.colorLight ?? '',
1435
+ ].join('|');
1436
+ }
1437
+ catch {
1438
+ return '';
1439
+ }
1440
+ }
1441
+ expandCollapsedHighlightRange(range) {
1442
+ try {
1443
+ if (!this.isCollapsed(range))
1444
+ return range;
1445
+ const row = this.normalizePosition(range.start).row;
1446
+ const line = this.readLine(row);
1447
+ const fallbackEnd = Math.max(range.end.column + 1, line.length);
1448
+ return {
1449
+ start: { row, column: 0 },
1450
+ end: { row, column: Math.max(1, fallbackEnd) },
1451
+ isBackwards: false,
1452
+ };
1453
+ }
1454
+ catch (error) {
1455
+ logError('Failed to expand collapsed highlight range', error);
1456
+ return range;
1457
+ }
1458
+ }
1459
+ readLine(row) {
1460
+ try {
1461
+ const safeRow = Math.max(0, Number(row) || 0);
1462
+ const session = this.session;
1463
+ const line = session?.getLine?.(safeRow);
1464
+ if (typeof line === 'string')
1465
+ return line;
1466
+ return this.readEditorValue().split('\n')[safeRow] ?? '';
1467
+ }
1468
+ catch {
1469
+ return '';
1470
+ }
1471
+ }
1472
+ normalizePosition(position) {
1473
+ try {
1474
+ return {
1475
+ row: Math.max(0, Number(position.row) || 0),
1476
+ column: Math.max(0, Number(position.column) || 0),
1477
+ };
1478
+ }
1479
+ catch {
1480
+ return { row: 0, column: 0 };
1481
+ }
1482
+ }
1483
+ isCollapsed(selection) {
1484
+ try {
1485
+ return selection.start?.row === selection.end?.row && selection.start?.column === selection.end?.column;
1486
+ }
1487
+ catch {
1488
+ return false;
1489
+ }
1490
+ }
1491
+ resolveCursorData() {
1492
+ try {
1493
+ const configured = this.config.cursorData ?? {};
1494
+ const user = this.readVeltUser();
1495
+ const color = configured.color || user?.color || DEFAULT_CURSOR_COLOR;
1496
+ return {
1497
+ name: configured.name || user?.name || user?.email || this.userId || 'Anonymous',
1498
+ email: configured.email || user?.email || '',
1499
+ color,
1500
+ colorLight: configured.colorLight || this.withAlpha(color, '33'),
1501
+ };
1502
+ }
1503
+ catch (error) {
1504
+ logError('Failed to resolve cursor data', error);
1505
+ return {
1506
+ name: 'Anonymous',
1507
+ email: '',
1508
+ color: DEFAULT_CURSOR_COLOR,
1509
+ colorLight: this.withAlpha(DEFAULT_CURSOR_COLOR, '33'),
1510
+ };
1511
+ }
1512
+ }
1513
+ readUserId() {
1514
+ try {
1515
+ return this.readVeltUser()?.userId || this.readVeltUser()?.id || 'anonymous';
1516
+ }
1517
+ catch {
1518
+ return 'anonymous';
1519
+ }
1520
+ }
1521
+ readVeltUser() {
1522
+ try {
1523
+ return this.config.veltClient?.getUser?.() ?? null;
1524
+ }
1525
+ catch {
1526
+ return null;
1527
+ }
1528
+ }
1529
+ withAlpha(color, alpha) {
1530
+ try {
1531
+ if (/^#[0-9a-fA-F]{6}$/.test(color))
1532
+ return `${color}${alpha}`;
1533
+ return `${DEFAULT_CURSOR_COLOR}${alpha}`;
1534
+ }
1535
+ catch {
1536
+ return `${DEFAULT_CURSOR_COLOR}${alpha}`;
1537
+ }
1538
+ }
1539
+ escapeCssColor(color) {
1540
+ try {
1541
+ return /^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(color) ? color : DEFAULT_CURSOR_COLOR;
1542
+ }
1543
+ catch {
1544
+ return DEFAULT_CURSOR_COLOR;
1545
+ }
1546
+ }
1547
+ escapeCssString(value) {
1548
+ try {
1549
+ return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
1550
+ }
1551
+ catch {
1552
+ return 'Remote';
1553
+ }
1554
+ }
1555
+ isCorruptionError(error) {
1556
+ try {
1557
+ const message = error instanceof Error ? error.message : String(error ?? '');
1558
+ return /invalid|corrupt|decode|unexpected end|malformed|contentRefs/i.test(message);
1559
+ }
1560
+ catch {
1561
+ return false;
1562
+ }
1563
+ }
1564
+ async tryRecoverFromCorruption() {
1565
+ try {
1566
+ logError('Detected corrupted CRDT state; attempting recovery');
1567
+ try {
1568
+ this.store?.destroy();
1569
+ }
1570
+ catch { /* ignore */ }
1571
+ this.store = this.createStore();
1572
+ this.setupAwareness();
1573
+ this.setupStoreTextObserver();
1574
+ this.setupAwarenessListeners();
1575
+ await this.store.initialize();
1576
+ }
1577
+ catch (error) {
1578
+ this.reportError('Failed to recover from corrupted CRDT state', error);
1579
+ }
1580
+ }
1581
+ reportError(message, error) {
1582
+ try {
1583
+ logError(message, error);
1584
+ this.config.onError?.(error instanceof Error ? error : new Error(message));
1585
+ }
1586
+ catch {
1587
+ // Never let error reporting throw.
1588
+ }
1589
+ }
1590
+ }
1591
+ async function createCollaboration(config) {
1592
+ try {
1593
+ const manager = new CollaborationManager(config);
1594
+ await manager.initialize();
1595
+ return manager;
1596
+ }
1597
+ catch (error) {
1598
+ logError('Failed to create collaboration', error);
1599
+ return new CollaborationManager(config);
1600
+ }
1601
+ }
1602
+
1603
+ exports.CollaborationManager = CollaborationManager;
1604
+ exports.createCollaboration = createCollaboration;
1605
+ exports.indexToPosition = indexToPosition;
1606
+ exports.normalizeText = normalizeText;
1607
+ exports.offsetsToRange = offsetsToRange;
1608
+ exports.positionToIndex = positionToIndex;
1609
+ exports.rangeToOffsets = rangeToOffsets;
1610
+ //# sourceMappingURL=index.js.map