@datalayer/lexical-loro 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +2 -2
  2. package/lib/Editor.js +3 -2
  3. package/lib/appSettings.js +1 -1
  4. package/lib/collab/loro/Bindings.js +7 -7
  5. package/lib/collab/loro/Debug.js +35 -35
  6. package/lib/collab/loro/LexicalCollaborationContext.js +1 -1
  7. package/lib/collab/loro/LexicalCollaborationPlugin.d.ts +6 -1
  8. package/lib/collab/loro/LexicalCollaborationPlugin.js +41 -13
  9. package/lib/collab/loro/State.js +1 -1
  10. package/lib/collab/loro/components/LoroCollaborators.js +7 -19
  11. package/lib/collab/loro/components/index.js +1 -1
  12. package/lib/collab/loro/index.js +1 -1
  13. package/lib/collab/loro/integrators/BaseIntegrator.js +1 -1
  14. package/lib/collab/loro/integrators/CounterIntegrator.js +1 -1
  15. package/lib/collab/loro/integrators/ListIntegrator.js +4 -4
  16. package/lib/collab/loro/integrators/MapIntegrator.js +4 -4
  17. package/lib/collab/loro/integrators/TextIntegrator.js +2 -2
  18. package/lib/collab/loro/integrators/TreeIntegrator.d.ts +5 -0
  19. package/lib/collab/loro/integrators/TreeIntegrator.js +184 -142
  20. package/lib/collab/loro/nodes/NodeFactory.js +26 -47
  21. package/lib/collab/loro/nodes/NodesMapper.js +19 -7
  22. package/lib/collab/loro/propagators/DecoratorNodePropagator.js +1 -1
  23. package/lib/collab/loro/propagators/ElementNodePropagator.d.ts +1 -1
  24. package/lib/collab/loro/propagators/ElementNodePropagator.js +102 -35
  25. package/lib/collab/loro/propagators/LineBreakNodePropagator.js +1 -1
  26. package/lib/collab/loro/propagators/RootNodePropagator.js +1 -1
  27. package/lib/collab/loro/propagators/TextNodePropagator.js +55 -45
  28. package/lib/collab/loro/propagators/index.d.ts +1 -1
  29. package/lib/collab/loro/propagators/index.js +1 -1
  30. package/lib/collab/loro/provider/websocket.d.ts +1 -0
  31. package/lib/collab/loro/provider/websocket.js +47 -12
  32. package/lib/collab/loro/servers/index.js +1 -1
  33. package/lib/collab/loro/servers/ws/callback.js +1 -1
  34. package/lib/collab/loro/servers/ws/server.js +1 -1
  35. package/lib/collab/loro/servers/ws/utils.js +35 -35
  36. package/lib/collab/loro/sync/SyncCursors.js +42 -16
  37. package/lib/collab/loro/sync/SyncLexicalToLoro.js +76 -37
  38. package/lib/collab/loro/sync/SyncLoroToLexical.js +32 -26
  39. package/lib/collab/loro/types/LexicalNodeData.js +1 -1
  40. package/lib/collab/loro/useCollaboration.js +173 -28
  41. package/lib/collab/loro/utils/InitialContent.js +1 -1
  42. package/lib/collab/loro/utils/Invariant.d.ts +15 -0
  43. package/lib/collab/loro/utils/Invariant.js +24 -0
  44. package/lib/collab/loro/utils/LexicalToLoro.js +1 -1
  45. package/lib/collab/loro/utils/Utils.js +1 -1
  46. package/lib/collab/loro/wsProvider.js +21 -4
  47. package/lib/collab/utils/invariant.js +1 -1
  48. package/lib/collab/utils/simpleDiffWithCursor.js +1 -1
  49. package/lib/commenting/index.js +1 -1
  50. package/lib/demo.js +1 -1
  51. package/lib/hooks/useFlashMessage.js +1 -1
  52. package/lib/hooks/useReport.js +1 -1
  53. package/lib/index.js +1 -1
  54. package/lib/nodes/AutocompleteNode.js +1 -1
  55. package/lib/nodes/EmojiNode.js +1 -1
  56. package/lib/nodes/KeywordNode.js +1 -1
  57. package/lib/nodes/LayoutContainerNode.js +1 -1
  58. package/lib/nodes/LayoutItemNode.js +1 -1
  59. package/lib/nodes/MentionNode.js +1 -1
  60. package/lib/nodes/PlaygroundNodes.js +1 -1
  61. package/lib/nodes/SpecialTextNode.js +1 -1
  62. package/lib/plugins/AutocompletePlugin/index.js +1 -1
  63. package/lib/plugins/CodeActionMenuPlugin/components/CopyButton/index.js +1 -1
  64. package/lib/plugins/CodeActionMenuPlugin/components/PrettierButton/index.js +1 -1
  65. package/lib/plugins/CodeActionMenuPlugin/utils.js +1 -1
  66. package/lib/plugins/CodeHighlightPrismPlugin/index.js +1 -1
  67. package/lib/plugins/CodeHighlightShikiPlugin/index.js +1 -1
  68. package/lib/plugins/CollapsiblePlugin/CollapsibleContainerNode.js +1 -1
  69. package/lib/plugins/CollapsiblePlugin/CollapsibleContentNode.js +1 -1
  70. package/lib/plugins/CollapsiblePlugin/CollapsibleTitleNode.js +1 -1
  71. package/lib/plugins/CollapsiblePlugin/CollapsibleUtils.js +1 -1
  72. package/lib/plugins/CollapsiblePlugin/index.js +1 -1
  73. package/lib/plugins/CounterPlugin/index.js +1 -1
  74. package/lib/plugins/DateTimePlugin/index.js +1 -1
  75. package/lib/plugins/DebugPlugin/index.js +59 -3
  76. package/lib/plugins/DragDropPastePlugin/index.js +1 -1
  77. package/lib/plugins/EmojiPickerPlugin/index.js +1 -1
  78. package/lib/plugins/EmojisPlugin/index.js +1 -1
  79. package/lib/plugins/FigmaPlugin/index.js +1 -1
  80. package/lib/plugins/KeywordsPlugin/index.js +1 -1
  81. package/lib/plugins/LayoutPlugin/LayoutPlugin.js +1 -1
  82. package/lib/plugins/MarkdownTransformers/index.js +1 -1
  83. package/lib/plugins/MaxLengthPlugin/index.js +1 -1
  84. package/lib/plugins/PageBreakPlugin/index.js +1 -1
  85. package/lib/plugins/ShortcutsPlugin/index.js +1 -1
  86. package/lib/plugins/ShortcutsPlugin/shortcuts.js +1 -1
  87. package/lib/plugins/SpecialTextPlugin/index.js +1 -1
  88. package/lib/plugins/SpeechToTextPlugin/index.js +1 -1
  89. package/lib/plugins/StickyPlugin/index.js +1 -1
  90. package/lib/plugins/TabFocusPlugin/index.js +1 -1
  91. package/lib/plugins/ToolbarPlugin/fontSize.js +1 -1
  92. package/lib/plugins/ToolbarPlugin/utils.js +1 -1
  93. package/lib/plugins/TwitterPlugin/index.js +1 -1
  94. package/lib/plugins/TypingPerfPlugin/index.js +1 -1
  95. package/lib/plugins/YouTubePlugin/index.js +1 -1
  96. package/lib/server/validation.js +1 -1
  97. package/lib/setupEnv.js +1 -1
  98. package/lib/themes/CommentEditorTheme.js +1 -1
  99. package/lib/themes/PlaygroundEditorTheme.js +1 -1
  100. package/lib/themes/StickyEditorTheme.js +1 -1
  101. package/lib/tyes.dt.js +1 -1
  102. package/lib/utils/docSerialization.js +1 -1
  103. package/lib/utils/emoji-list.js +1 -1
  104. package/lib/utils/getDOMRangeRect.js +1 -1
  105. package/lib/utils/getSelectedNode.js +1 -1
  106. package/lib/utils/getThemeSelector.js +1 -1
  107. package/lib/utils/isMobileWidth.js +1 -1
  108. package/lib/utils/joinClasses.js +1 -1
  109. package/lib/utils/setFloatingElemPosition.js +1 -1
  110. package/lib/utils/setFloatingElemPositionForLinkEditor.js +1 -1
  111. package/lib/utils/swipe.js +1 -1
  112. package/lib/utils/url.js +1 -1
  113. package/package.json +4 -4
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /*
3
- * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Copyright (c) 2025-2026 Datalayer, Inc.
4
4
  * Distributed under the terms of the MIT License.
5
5
  */
6
6
  import * as React from 'react';
7
7
  import { useCallback, useEffect, useMemo, useRef } from 'react';
8
8
  import { createPortal } from 'react-dom';
9
9
  import { mergeRegister } from '@lexical/utils';
10
- import { $getRoot, BLUR_COMMAND, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_EDITOR, FOCUS_COMMAND, HISTORY_MERGE_TAG, REDO_COMMAND, SKIP_COLLAB_TAG, UNDO_COMMAND, } from 'lexical';
10
+ import { $getNodeByKey, $getRoot, $parseSerializedNode, $setSelection, BLUR_COMMAND, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_EDITOR, FOCUS_COMMAND, HISTORY_MERGE_TAG, REDO_COMMAND, SKIP_COLLAB_TAG, UNDO_COMMAND, } from 'lexical';
11
11
  import { CONNECTED_COMMAND, createUndoManager, initLocalState, setLocalStateFocus, TOGGLE_CONNECT_COMMAND, } from './State';
12
12
  import { syncLexicalToLoro } from './sync/SyncLexicalToLoro';
13
13
  import { syncLoroToLexical } from './sync/SyncLoroToLexical';
@@ -29,6 +29,7 @@ export function useCollaboration(editor, id, provider, docMap, name, color, shou
29
29
  editor.dispatchCommand(CONNECTED_COMMAND, status === 'connected');
30
30
  };
31
31
  const onSync = (isSynced) => {
32
+ console.log('[SEED-DEBUG] onSync: isSynced=', isSynced, 'shouldBootstrap=', shouldBootstrap, 'isReloadingDoc=', isReloadingDoc.current);
32
33
  if (shouldBootstrap &&
33
34
  isSynced &&
34
35
  isReloadingDoc.current === false) {
@@ -67,7 +68,41 @@ export function useCollaboration(editor, id, provider, docMap, name, color, shou
67
68
  };
68
69
  // This updates the local editor state when we receive updates from other clients.
69
70
  const unsubscribe = binding.doc.subscribe(onLoroUpdates);
71
+ // Lexical only includes a node type in `update.mutatedNodes` when that type
72
+ // has at least one registered mutation listener. Our Lexical→Loro sync
73
+ // relies entirely on `mutatedNodes`, so any node type without a listener is
74
+ // invisible to collaboration. In particular, the bootstrap seed runs before
75
+ // other plugins mount and register their own mutation listeners, so its
76
+ // commits produced an empty `mutatedNodes` and never propagated to peers.
77
+ // Register a no-op mutation listener for every node type registered on this
78
+ // editor so `mutatedNodes` is always fully populated (this mirrors how the
79
+ // Yjs binding stays independent of other plugins).
80
+ const mutationListenerCleanups = [];
81
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
+ const registeredNodes = editor._nodes;
83
+ console.log('[SEED-DEBUG] registering mutation listeners; registeredNodes=', registeredNodes ? registeredNodes.size : 'undefined');
84
+ if (registeredNodes) {
85
+ registeredNodes.forEach((registered) => {
86
+ if (registered && registered.klass) {
87
+ mutationListenerCleanups.push(
88
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
89
+ editor.registerMutationListener(registered.klass, () => { }, {
90
+ skipInitialization: true,
91
+ }));
92
+ }
93
+ });
94
+ }
95
+ console.log('[SEED-DEBUG] mutation listeners registered=', mutationListenerCleanups.length, 'editor._listeners.mutation.size=',
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ editor._listeners?.mutation?.size);
70
98
  const removeListener = editor.registerUpdateListener((update) => {
99
+ let __mn = 0;
100
+ if (update.mutatedNodes) {
101
+ update.mutatedNodes.forEach((m) => {
102
+ __mn += m.size;
103
+ });
104
+ }
105
+ console.log('[SEED-DEBUG] updateListener fired; tags=', Array.from(update.tags), 'mutatedNodes=', update.mutatedNodes ? 'map' : 'null', 'count=', __mn, 'dirtyElements=', update.dirtyElements?.size, 'dirtyLeaves=', update.dirtyLeaves?.size);
71
106
  if (update.tags.has(SKIP_COLLAB_TAG) === false) {
72
107
  syncLexicalToLoro(binding, provider, update);
73
108
  }
@@ -97,6 +132,7 @@ export function useCollaboration(editor, id, provider, docMap, name, color, shou
97
132
  unsubscribe?.();
98
133
  docMap.delete(id);
99
134
  removeListener();
135
+ mutationListenerCleanups.forEach((cleanup) => cleanup());
100
136
  };
101
137
  }, [
102
138
  binding,
@@ -193,34 +229,143 @@ export function useHistory(editor, binding) {
193
229
  return clearHistory;
194
230
  }
195
231
  function initializeEditor(editor, initialEditorState) {
196
- editor.update(() => {
232
+ // Text used by the server-side default scaffold that fresh rooms are
233
+ // pre-seeded with. When a room only contains this scaffold it should be
234
+ // treated as empty so the host app's real initial content can replace it.
235
+ const PLACEHOLDER_TEXTS = new Set([
236
+ 'Lexical with Loro',
237
+ 'Welcome to Lexical with Loro',
238
+ 'Type something...',
239
+ ]);
240
+ const hasMeaningfulContent = () => {
197
241
  const root = $getRoot();
198
- if (root.isEmpty()) {
199
- if (initialEditorState) {
200
- switch (typeof initialEditorState) {
201
- case 'string': {
202
- const parsedEditorState = editor.parseEditorState(initialEditorState);
203
- editor.setEditorState(parsedEditorState, {
204
- tag: HISTORY_MERGE_TAG,
205
- });
206
- break;
207
- }
208
- case 'object': {
209
- editor.setEditorState(initialEditorState, {
210
- tag: HISTORY_MERGE_TAG,
211
- });
212
- break;
213
- }
214
- case 'function': {
215
- editor.update(() => {
216
- const root1 = $getRoot();
217
- if (root1.isEmpty()) {
218
- initialEditorState(editor);
219
- }
220
- }, { tag: HISTORY_MERGE_TAG });
221
- break;
222
- }
242
+ const children = root.getChildren();
243
+ if (children.length === 0) {
244
+ return false;
245
+ }
246
+ for (const child of children) {
247
+ const type = child.getType();
248
+ const text = child.getTextContent().trim();
249
+ if (text.length > 0) {
250
+ // Ignore the known default scaffold text so it can be overwritten by
251
+ // the host app's initial content on a freshly created room.
252
+ if (!PLACEHOLDER_TEXTS.has(text)) {
253
+ return true;
254
+ }
255
+ continue;
256
+ }
257
+ // Ignore known scaffold placeholders used by some host apps before
258
+ // collaboration bootstrap fills the real initial document.
259
+ if (type !== 'paragraph' && type !== 'jupyter-output') {
260
+ return true;
261
+ }
262
+ }
263
+ return false;
264
+ };
265
+ // Only seed a room whose sole content is the (empty/scaffold) default.
266
+ let shouldSeed = false;
267
+ editor.getEditorState().read(() => {
268
+ shouldSeed = !hasMeaningfulContent();
269
+ });
270
+ console.log('[SEED-DEBUG] initializeEditor: shouldSeed=', shouldSeed, 'hasInitialState=', !!initialEditorState, 'type=', typeof initialEditorState);
271
+ if (!shouldSeed || !initialEditorState) {
272
+ return;
273
+ }
274
+ // Preferred path: a serialized editor state (string). Seed it INCREMENTALLY,
275
+ // exactly as a human collaborator would build the document — append each
276
+ // top-level block in its own commit, then remove the scaffold blocks one by
277
+ // one. Each small, well-formed commit produces clean per-node "created"
278
+ // mutations that the Loro binding propagates reliably. A single bulk
279
+ // clear()+append emitted one large, malformed batch that failed to integrate
280
+ // on remote peers (inline nodes arriving without a resolved parent), which is
281
+ // why the second editor never received the initial content.
282
+ if (typeof initialEditorState === 'string') {
283
+ let serializedChildren = [];
284
+ try {
285
+ const serialized = JSON.parse(initialEditorState);
286
+ serializedChildren = serialized?.root?.children ?? [];
287
+ }
288
+ catch (error) {
289
+ // Not a rebuildable serialized state → fall back to a whole-state swap.
290
+ editor.update(() => {
291
+ editor.setEditorState(editor.parseEditorState(initialEditorState), {
292
+ tag: HISTORY_MERGE_TAG,
293
+ });
294
+ }, { tag: HISTORY_MERGE_TAG });
295
+ return;
296
+ }
297
+ // IMPORTANT: every seed update below is committed with `discrete: true`.
298
+ // The remote snapshot import runs `editor.update({tag: SKIP_COLLAB_TAG})`
299
+ // and that update is still queued when this bootstrap runs. Without
300
+ // `discrete`, Lexical coalesces the queued remote update together with our
301
+ // seed updates into a single reconciliation and unions their tags, so the
302
+ // seed batch inherits `skip-collab` and the collab sync ignores it. A
303
+ // leading discrete no-op flushes the pending remote (skip-collab) update on
304
+ // its own, and each discrete seed update then commits as its own
305
+ // reconciliation carrying only `history-merge`, so it propagates to peers.
306
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
307
+ editor.update(() => { }, { tag: HISTORY_MERGE_TAG, discrete: true });
308
+ // Clear any pending selection first: it may point at scaffold nodes we are
309
+ // about to remove, which would otherwise crash selection-reading plugins
310
+ // (e.g. the typeahead menu).
311
+ editor.update(() => {
312
+ $setSelection(null);
313
+ }, { tag: HISTORY_MERGE_TAG, discrete: true });
314
+ // Capture the scaffold blocks AFTER the leading discrete flush, so any
315
+ // late-arriving remote scaffold nodes are included. We remove them BEFORE
316
+ // appending the real content: the scaffold's position in the Loro tree does
317
+ // not necessarily line up with its Lexical index, so appending after it
318
+ // makes the computed Loro insertion index drift out of range. Removing the
319
+ // scaffold first leaves both trees at an aligned empty root, after which
320
+ // each appended block sits at a matching index in Lexical and Loro.
321
+ let scaffoldKeys = [];
322
+ editor.getEditorState().read(() => {
323
+ scaffoldKeys = $getRoot()
324
+ .getChildren()
325
+ .map(child => child.getKey());
326
+ });
327
+ console.log('[SEED-DEBUG] initializeEditor: seeding', serializedChildren.length, 'blocks, removing', scaffoldKeys.length, 'scaffold blocks first');
328
+ // Remove the scaffold blocks one by one so both trees reach an aligned
329
+ // empty root before we append the real content.
330
+ for (const key of scaffoldKeys) {
331
+ editor.update(() => {
332
+ const node = $getNodeByKey(key);
333
+ if (node) {
334
+ node.remove();
223
335
  }
336
+ }, { tag: HISTORY_MERGE_TAG, discrete: true });
337
+ }
338
+ // Append the real content block by block into the now-empty, aligned root
339
+ // (each commit ≈ one collaborator action).
340
+ let __blockIndex = 0;
341
+ for (const serializedNode of serializedChildren) {
342
+ const __idx = __blockIndex++;
343
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
344
+ const __type = serializedNode?.type;
345
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
346
+ const __childTypes = Array.isArray(serializedNode?.children)
347
+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
348
+ serializedNode.children.map((c) => c?.type)
349
+ : [];
350
+ editor.update(() => {
351
+ const root = $getRoot();
352
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
353
+ root.append($parseSerializedNode(serializedNode));
354
+ }, { tag: HISTORY_MERGE_TAG, discrete: true });
355
+ console.log('[SEED-DEBUG] appended block', __idx, 'type=', __type, 'children=', __childTypes);
356
+ }
357
+ return;
358
+ }
359
+ // Non-string fallbacks (EditorState object or builder function).
360
+ editor.update(() => {
361
+ if (!hasMeaningfulContent()) {
362
+ if (typeof initialEditorState === 'object') {
363
+ editor.setEditorState(initialEditorState, {
364
+ tag: HISTORY_MERGE_TAG,
365
+ });
366
+ }
367
+ else if (typeof initialEditorState === 'function') {
368
+ initialEditorState(editor);
224
369
  }
225
370
  }
226
371
  }, { tag: HISTORY_MERGE_TAG });
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { lexicalToLoroTree } from './LexicalToLoro';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Assert a collaboration invariant.
3
+ *
4
+ * The Loro <-> Lexical sync engine relies on a set of structural invariants
5
+ * (a node's parent is already mapped, an insertion index is within range, a
6
+ * text node always has a parent, ...). When one of these does not hold the
7
+ * engine is in a state it cannot resolve correctly.
8
+ *
9
+ * Historically such cases were "handled" by logging a warning and skipping the
10
+ * operation (or clamping an index). That silently corrupts the shared document
11
+ * and hides the underlying bug. Instead we throw: the failure surfaces
12
+ * immediately, the surrounding `editor.update()` rolls back cleanly, and the
13
+ * root cause can be found and fixed rather than masked.
14
+ */
15
+ export declare function invariant(condition: unknown, message: string, context?: Record<string, unknown>): asserts condition;
@@ -0,0 +1,24 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the MIT License.
4
+ */
5
+ /**
6
+ * Assert a collaboration invariant.
7
+ *
8
+ * The Loro <-> Lexical sync engine relies on a set of structural invariants
9
+ * (a node's parent is already mapped, an insertion index is within range, a
10
+ * text node always has a parent, ...). When one of these does not hold the
11
+ * engine is in a state it cannot resolve correctly.
12
+ *
13
+ * Historically such cases were "handled" by logging a warning and skipping the
14
+ * operation (or clamping an index). That silently corrupts the shared document
15
+ * and hides the underlying bug. Instead we throw: the failure surfaces
16
+ * immediately, the surrounding `editor.update()` rolls back cleanly, and the
17
+ * root cause can be found and fixed rather than masked.
18
+ */
19
+ export function invariant(condition, message, context) {
20
+ if (!condition) {
21
+ const details = context ? ` ${JSON.stringify(context)}` : '';
22
+ throw new Error(`[loro-collab] Invariant failed: ${message}${details}`);
23
+ }
24
+ }
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { $getNodeByKey, $getRoot, $getSelection, $isRangeSelection, $isTextNode } from 'lexical';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { LoroDoc } from 'loro-crdt';
@@ -19,17 +19,34 @@ export function createWebsocketProvider(id, docMap, websocketUrl) {
19
19
  docMap.set(id, doc);
20
20
  }
21
21
  const providerInstanceId = Math.random().toString(36).substr(2, 9);
22
- console.log(`🏭 Creating WebsocketProvider instance (ID: ${providerInstanceId}) for docId: ${id}`);
22
+ console.log(` Creating WebsocketProvider instance (ID: ${providerInstanceId}) for docId: ${id}`);
23
23
  // Use provided websocketUrl or fallback to URL parameters/defaults
24
- const finalWebsocketUrl = websocketUrl || (() => {
24
+ const rawWebsocketUrl = websocketUrl || (() => {
25
25
  const url = new URL(window.location.href);
26
26
  const params = new URLSearchParams(url.search);
27
27
  return params.get('collabEndpoint') || 'ws://localhost:3002';
28
28
  })();
29
+ // The WebsocketProvider builds the connection URL as
30
+ // `serverUrl + '/' + docId + '?' + params`. If the caller embedded a query
31
+ // string (for example `?token=<jwt>`) directly in the URL, the docId would be
32
+ // appended *after* the query string, corrupting the URL. Split any query
33
+ // string off the base URL and forward it through the provider `params` so the
34
+ // final URL is `serverUrl/docId?token=<jwt>` as the server expects.
35
+ let finalWebsocketUrl = rawWebsocketUrl;
36
+ const params = {};
37
+ const queryIndex = finalWebsocketUrl.indexOf('?');
38
+ if (queryIndex !== -1) {
39
+ const query = finalWebsocketUrl.slice(queryIndex + 1);
40
+ finalWebsocketUrl = finalWebsocketUrl.slice(0, queryIndex);
41
+ new URLSearchParams(query).forEach((value, key) => {
42
+ params[key] = value;
43
+ });
44
+ }
29
45
  const websocketProvider = new WebsocketProvider(finalWebsocketUrl, id, doc, {
30
46
  connect: false,
47
+ params,
31
48
  resyncInterval: RESYNC_INTERVAL, // Poll ephemeral state periodically to prevent stale user accumulation
32
49
  });
33
- console.log(`🏭 WebsocketProvider created for: ${finalWebsocketUrl}/${id} with resyncInterval: ${RESYNC_INTERVAL}ms`);
50
+ console.log(` WebsocketProvider created for: ${finalWebsocketUrl}/${id} with resyncInterval: ${RESYNC_INTERVAL}ms`);
34
51
  return websocketProvider;
35
52
  }
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  // invariant(condition, message) will refine types based on "condition", and
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  export default function simpleDiffWithCursor(a, b, cursor) {
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { COMMAND_PRIORITY_LOW } from 'lexical';
package/lib/demo.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /*
3
- * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Copyright (c) 2025-2026 Datalayer, Inc.
4
4
  * Distributed under the terms of the MIT License.
5
5
  */
6
6
  import { createRoot } from 'react-dom/client';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { useFlashMessageContext, } from '../context/FlashMessageContext';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { useCallback, useEffect, useRef } from 'react';
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  export * from './collab/loro';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { TextNode } from 'lexical';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { $applyNodeReplacement, TextNode } from 'lexical';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { $applyNodeReplacement, TextNode } from 'lexical';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { addClassNamesToElement } from '@lexical/utils';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { addClassNamesToElement } from '@lexical/utils';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { $applyNodeReplacement, TextNode, } from 'lexical';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { CodeHighlightNode, CodeNode } from '@lexical/code';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { addClassNamesToElement } from '@lexical/utils';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /*
3
- * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Copyright (c) 2025-2026 Datalayer, Inc.
4
4
  * Distributed under the terms of the MIT License.
5
5
  */
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /*
3
- * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Copyright (c) 2025-2026 Datalayer, Inc.
4
4
  * Distributed under the terms of the MIT License.
5
5
  */
6
6
  /**
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { registerCodeHighlighting } from '@lexical/code';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { registerCodeHighlighting } from '@lexical/code-shiki';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { IS_CHROME } from '@lexical/utils';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { IS_CHROME } from '@lexical/utils';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { IS_CHROME } from '@lexical/utils';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  export function setDomHiddenUntilFound(dom) {
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import './Collapsible.css';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright (c) 2023-2025 Datalayer, Inc.
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
3
  * Distributed under the terms of the MIT License.
4
4
  */
5
5
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';