@datalayer/lexical-loro 1.0.2 → 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 (402) hide show
  1. package/README.md +2 -2
  2. package/lib/App.d.ts +2 -0
  3. package/lib/App.js +141 -0
  4. package/lib/Editor.d.ts +2 -0
  5. package/lib/Editor.js +116 -0
  6. package/lib/Settings.d.ts +2 -0
  7. package/lib/Settings.js +57 -0
  8. package/lib/appSettings.d.ts +36 -0
  9. package/lib/appSettings.js +48 -0
  10. package/lib/collab/loro/Bindings.d.ts +41 -0
  11. package/lib/collab/loro/Bindings.js +99 -0
  12. package/lib/collab/loro/Debug.d.ts +33 -0
  13. package/lib/collab/loro/Debug.js +452 -0
  14. package/lib/collab/loro/LexicalCollaborationContext.d.ts +19 -0
  15. package/lib/collab/loro/LexicalCollaborationContext.js +52 -0
  16. package/lib/collab/loro/LexicalCollaborationPlugin.d.ts +29 -0
  17. package/lib/collab/loro/LexicalCollaborationPlugin.js +111 -0
  18. package/lib/collab/loro/State.d.ts +53 -0
  19. package/lib/collab/loro/State.js +94 -0
  20. package/lib/collab/loro/components/LoroCollaborationUI.d.ts +13 -0
  21. package/lib/collab/loro/components/LoroCollaborationUI.js +9 -0
  22. package/lib/collab/loro/components/LoroCollaborators.d.ts +8 -0
  23. package/lib/collab/loro/components/LoroCollaborators.js +90 -0
  24. package/lib/collab/loro/components/index.d.ts +2 -0
  25. package/lib/collab/loro/components/index.js +6 -0
  26. package/lib/collab/loro/index.d.ts +6 -0
  27. package/lib/collab/loro/index.js +10 -0
  28. package/lib/collab/loro/integrators/BaseIntegrator.d.ts +14 -0
  29. package/lib/collab/loro/integrators/BaseIntegrator.js +5 -0
  30. package/lib/collab/loro/integrators/CounterIntegrator.d.ts +23 -0
  31. package/lib/collab/loro/integrators/CounterIntegrator.js +44 -0
  32. package/lib/collab/loro/integrators/ListIntegrator.d.ts +23 -0
  33. package/lib/collab/loro/integrators/ListIntegrator.js +53 -0
  34. package/lib/collab/loro/integrators/MapIntegrator.d.ts +24 -0
  35. package/lib/collab/loro/integrators/MapIntegrator.js +235 -0
  36. package/lib/collab/loro/integrators/TextIntegrator.d.ts +25 -0
  37. package/lib/collab/loro/integrators/TextIntegrator.js +55 -0
  38. package/lib/collab/loro/integrators/TreeIntegrator.d.ts +41 -0
  39. package/lib/collab/loro/integrators/TreeIntegrator.js +293 -0
  40. package/lib/collab/loro/nodes/NodeFactory.d.ts +15 -0
  41. package/lib/collab/loro/nodes/NodeFactory.js +80 -0
  42. package/lib/collab/loro/nodes/NodesMapper.d.ts +120 -0
  43. package/lib/collab/loro/nodes/NodesMapper.js +289 -0
  44. package/lib/collab/loro/propagators/DecoratorNodePropagator.d.ts +60 -0
  45. package/lib/collab/loro/propagators/DecoratorNodePropagator.js +306 -0
  46. package/lib/collab/loro/propagators/ElementNodePropagator.d.ts +62 -0
  47. package/lib/collab/loro/propagators/ElementNodePropagator.js +393 -0
  48. package/lib/collab/loro/propagators/LineBreakNodePropagator.d.ts +57 -0
  49. package/lib/collab/loro/propagators/LineBreakNodePropagator.js +200 -0
  50. package/lib/collab/loro/propagators/RootNodePropagator.d.ts +55 -0
  51. package/lib/collab/loro/propagators/RootNodePropagator.js +174 -0
  52. package/lib/collab/loro/propagators/TextNodePropagator.d.ts +60 -0
  53. package/lib/collab/loro/propagators/TextNodePropagator.js +450 -0
  54. package/lib/collab/loro/propagators/index.d.ts +49 -0
  55. package/lib/collab/loro/propagators/index.js +30 -0
  56. package/lib/collab/loro/provider/websocket.d.ts +117 -0
  57. package/lib/collab/loro/provider/websocket.js +946 -0
  58. package/lib/collab/loro/servers/index.d.ts +0 -0
  59. package/lib/collab/loro/servers/index.js +4 -0
  60. package/lib/collab/loro/servers/ws/callback.d.ts +5 -0
  61. package/lib/collab/loro/servers/ws/callback.js +89 -0
  62. package/lib/collab/loro/servers/ws/server.d.ts +2 -0
  63. package/lib/collab/loro/servers/ws/server.js +29 -0
  64. package/lib/collab/loro/servers/ws/utils.d.ts +40 -0
  65. package/lib/collab/loro/servers/ws/utils.js +517 -0
  66. package/lib/collab/loro/sync/SyncCursors.d.ts +32 -0
  67. package/lib/collab/loro/sync/SyncCursors.js +501 -0
  68. package/lib/collab/loro/sync/SyncLexicalToLoro.d.ts +4 -0
  69. package/lib/collab/loro/sync/SyncLexicalToLoro.js +152 -0
  70. package/lib/collab/loro/sync/SyncLoroToLexical.d.ts +5 -0
  71. package/lib/collab/loro/sync/SyncLoroToLexical.js +106 -0
  72. package/lib/collab/loro/types/LexicalNodeData.d.ts +32 -0
  73. package/lib/collab/loro/types/LexicalNodeData.js +75 -0
  74. package/lib/collab/loro/useCollaboration.d.ts +12 -0
  75. package/lib/collab/loro/useCollaboration.js +405 -0
  76. package/lib/collab/loro/utils/InitialContent.d.ts +64 -0
  77. package/lib/collab/loro/utils/InitialContent.js +113 -0
  78. package/lib/collab/loro/utils/Invariant.d.ts +15 -0
  79. package/lib/collab/loro/utils/Invariant.js +24 -0
  80. package/lib/collab/loro/utils/LexicalToLoro.d.ts +18 -0
  81. package/lib/collab/loro/utils/LexicalToLoro.js +100 -0
  82. package/lib/collab/loro/utils/Utils.d.ts +44 -0
  83. package/lib/collab/loro/utils/Utils.js +157 -0
  84. package/lib/collab/loro/wsProvider.d.ts +8 -0
  85. package/lib/collab/loro/wsProvider.js +52 -0
  86. package/lib/collab/utils/invariant.d.ts +1 -0
  87. package/lib/collab/utils/invariant.js +15 -0
  88. package/lib/collab/utils/simpleDiffWithCursor.d.ts +5 -0
  89. package/lib/collab/utils/simpleDiffWithCursor.js +35 -0
  90. package/lib/collab/yjs/Bindings.d.ts +23 -0
  91. package/lib/collab/yjs/Bindings.js +26 -0
  92. package/lib/collab/yjs/Debug.d.ts +23 -0
  93. package/lib/collab/yjs/Debug.js +213 -0
  94. package/lib/collab/yjs/LexicalCollaborationContext.d.ts +10 -0
  95. package/lib/collab/yjs/LexicalCollaborationContext.js +37 -0
  96. package/lib/collab/yjs/LexicalCollaborationPlugin.d.ts +21 -0
  97. package/lib/collab/yjs/LexicalCollaborationPlugin.js +63 -0
  98. package/lib/collab/yjs/State.d.ts +51 -0
  99. package/lib/collab/yjs/State.js +35 -0
  100. package/lib/collab/yjs/nodes/AnyCollabNode.d.ts +5 -0
  101. package/lib/collab/yjs/nodes/AnyCollabNode.js +1 -0
  102. package/lib/collab/yjs/nodes/CollabDecoratorNode.d.ts +22 -0
  103. package/lib/collab/yjs/nodes/CollabDecoratorNode.js +64 -0
  104. package/lib/collab/yjs/nodes/CollabElementNode.d.ts +40 -0
  105. package/lib/collab/yjs/nodes/CollabElementNode.js +462 -0
  106. package/lib/collab/yjs/nodes/CollabLineBreakNode.d.ts +19 -0
  107. package/lib/collab/yjs/nodes/CollabLineBreakNode.js +44 -0
  108. package/lib/collab/yjs/nodes/CollabTextNode.d.ts +25 -0
  109. package/lib/collab/yjs/nodes/CollabTextNode.js +103 -0
  110. package/lib/collab/yjs/provider/websocket.d.ts +88 -0
  111. package/lib/collab/yjs/provider/websocket.js +415 -0
  112. package/lib/collab/yjs/servers/index.d.ts +0 -0
  113. package/lib/collab/yjs/servers/index.js +0 -0
  114. package/lib/collab/yjs/servers/ws/callback.d.ts +5 -0
  115. package/lib/collab/yjs/servers/ws/callback.js +72 -0
  116. package/lib/collab/yjs/servers/ws/server.d.ts +2 -0
  117. package/lib/collab/yjs/servers/ws/server.js +25 -0
  118. package/lib/collab/yjs/servers/ws/utils.d.ts +49 -0
  119. package/lib/collab/yjs/servers/ws/utils.js +284 -0
  120. package/lib/collab/yjs/sync/SyncCursors.d.ts +39 -0
  121. package/lib/collab/yjs/sync/SyncCursors.js +351 -0
  122. package/lib/collab/yjs/sync/SyncEditorStates.d.ts +10 -0
  123. package/lib/collab/yjs/sync/SyncEditorStates.js +200 -0
  124. package/lib/collab/yjs/useCollaboration.d.ts +12 -0
  125. package/lib/collab/yjs/useCollaboration.js +255 -0
  126. package/lib/collab/yjs/utils/Utils.d.ts +25 -0
  127. package/lib/collab/yjs/utils/Utils.js +402 -0
  128. package/lib/collab/yjs/wsProvider.d.ts +3 -0
  129. package/lib/collab/yjs/wsProvider.js +21 -0
  130. package/lib/commenting/index.d.ts +41 -0
  131. package/lib/commenting/index.js +328 -0
  132. package/lib/context/FlashMessageContext.d.ts +7 -0
  133. package/lib/context/FlashMessageContext.js +24 -0
  134. package/lib/context/SettingsContext.d.ts +12 -0
  135. package/lib/context/SettingsContext.js +38 -0
  136. package/lib/context/SharedHistoryContext.d.ts +11 -0
  137. package/lib/context/SharedHistoryContext.js +11 -0
  138. package/lib/context/ToolbarContext.d.ts +65 -0
  139. package/lib/context/ToolbarContext.js +84 -0
  140. package/lib/demo.d.ts +12 -0
  141. package/lib/demo.js +45 -0
  142. package/lib/hooks/useFlashMessage.d.ts +2 -0
  143. package/lib/hooks/useFlashMessage.js +8 -0
  144. package/lib/hooks/useModal.d.ts +5 -0
  145. package/lib/hooks/useModal.js +26 -0
  146. package/lib/hooks/useReport.d.ts +1 -0
  147. package/lib/hooks/useReport.js +50 -0
  148. package/lib/index.d.ts +1 -0
  149. package/lib/index.js +5 -0
  150. package/lib/nodes/AutocompleteNode.d.ts +27 -0
  151. package/lib/nodes/AutocompleteNode.js +60 -0
  152. package/lib/nodes/CounterComponent.d.ts +6 -0
  153. package/lib/nodes/CounterComponent.js +137 -0
  154. package/lib/nodes/CounterNode.d.ts +23 -0
  155. package/lib/nodes/CounterNode.js +47 -0
  156. package/lib/nodes/DateTimeNode/DateTimeComponent.d.ts +8 -0
  157. package/lib/nodes/DateTimeNode/DateTimeComponent.js +119 -0
  158. package/lib/nodes/DateTimeNode/DateTimeNode.d.ts +27 -0
  159. package/lib/nodes/DateTimeNode/DateTimeNode.js +82 -0
  160. package/lib/nodes/EmojiNode.d.ts +18 -0
  161. package/lib/nodes/EmojiNode.js +54 -0
  162. package/lib/nodes/EquationComponent.d.ts +9 -0
  163. package/lib/nodes/EquationComponent.js +75 -0
  164. package/lib/nodes/EquationNode.d.ts +26 -0
  165. package/lib/nodes/EquationNode.js +109 -0
  166. package/lib/nodes/ExcalidrawNode/ExcalidrawComponent.d.ts +8 -0
  167. package/lib/nodes/ExcalidrawNode/ExcalidrawComponent.js +110 -0
  168. package/lib/nodes/ExcalidrawNode/ExcalidrawImage.d.ts +50 -0
  169. package/lib/nodes/ExcalidrawNode/ExcalidrawImage.js +55 -0
  170. package/lib/nodes/ExcalidrawNode/index.d.ts +32 -0
  171. package/lib/nodes/ExcalidrawNode/index.js +117 -0
  172. package/lib/nodes/FigmaNode.d.ts +20 -0
  173. package/lib/nodes/FigmaNode.js +52 -0
  174. package/lib/nodes/ImageComponent.d.ts +16 -0
  175. package/lib/nodes/ImageComponent.js +272 -0
  176. package/lib/nodes/ImageNode.d.ts +50 -0
  177. package/lib/nodes/ImageNode.js +151 -0
  178. package/lib/nodes/InlineImageNode/InlineImageComponent.d.ts +26 -0
  179. package/lib/nodes/InlineImageNode/InlineImageComponent.js +161 -0
  180. package/lib/nodes/InlineImageNode/InlineImageNode.d.ts +59 -0
  181. package/lib/nodes/InlineImageNode/InlineImageNode.js +162 -0
  182. package/lib/nodes/KeywordNode.d.ts +14 -0
  183. package/lib/nodes/KeywordNode.js +37 -0
  184. package/lib/nodes/LayoutContainerNode.d.ts +24 -0
  185. package/lib/nodes/LayoutContainerNode.js +95 -0
  186. package/lib/nodes/LayoutItemNode.d.ts +16 -0
  187. package/lib/nodes/LayoutItemNode.js +69 -0
  188. package/lib/nodes/MentionNode.d.ts +20 -0
  189. package/lib/nodes/MentionNode.js +85 -0
  190. package/lib/nodes/PageBreakNode/index.d.ts +17 -0
  191. package/lib/nodes/PageBreakNode/index.js +83 -0
  192. package/lib/nodes/PlaygroundNodes.d.ts +3 -0
  193. package/lib/nodes/PlaygroundNodes.js +75 -0
  194. package/lib/nodes/PollComponent.d.ts +9 -0
  195. package/lib/nodes/PollComponent.js +85 -0
  196. package/lib/nodes/PollNode.d.ts +43 -0
  197. package/lib/nodes/PollNode.js +153 -0
  198. package/lib/nodes/SpecialTextNode.d.ts +24 -0
  199. package/lib/nodes/SpecialTextNode.js +54 -0
  200. package/lib/nodes/StickyComponent.d.ts +10 -0
  201. package/lib/nodes/StickyComponent.js +162 -0
  202. package/lib/nodes/StickyNode.d.ts +31 -0
  203. package/lib/nodes/StickyNode.js +76 -0
  204. package/lib/nodes/TweetNode.d.ts +21 -0
  205. package/lib/nodes/TweetNode.js +119 -0
  206. package/lib/nodes/YouTubeNode.d.ts +22 -0
  207. package/lib/nodes/YouTubeNode.js +84 -0
  208. package/lib/plugins/ActionsPlugin/index.d.ts +5 -0
  209. package/lib/plugins/ActionsPlugin/index.js +168 -0
  210. package/lib/plugins/AutoEmbedPlugin/index.d.ts +19 -0
  211. package/lib/plugins/AutoEmbedPlugin/index.js +158 -0
  212. package/lib/plugins/AutoLinkPlugin/index.d.ts +2 -0
  213. package/lib/plugins/AutoLinkPlugin/index.js +15 -0
  214. package/lib/plugins/AutocompletePlugin/index.d.ts +10 -0
  215. package/lib/plugins/AutocompletePlugin/index.js +2477 -0
  216. package/lib/plugins/CodeActionMenuPlugin/components/CopyButton/index.d.ts +7 -0
  217. package/lib/plugins/CodeActionMenuPlugin/components/CopyButton/index.js +46 -0
  218. package/lib/plugins/CodeActionMenuPlugin/components/PrettierButton/index.d.ts +17 -0
  219. package/lib/plugins/CodeActionMenuPlugin/components/PrettierButton/index.js +115 -0
  220. package/lib/plugins/CodeActionMenuPlugin/index.d.ts +5 -0
  221. package/lib/plugins/CodeActionMenuPlugin/index.js +104 -0
  222. package/lib/plugins/CodeActionMenuPlugin/utils.d.ts +1 -0
  223. package/lib/plugins/CodeActionMenuPlugin/utils.js +22 -0
  224. package/lib/plugins/CodeHighlightPrismPlugin/index.d.ts +2 -0
  225. package/lib/plugins/CodeHighlightPrismPlugin/index.js +14 -0
  226. package/lib/plugins/CodeHighlightShikiPlugin/index.d.ts +2 -0
  227. package/lib/plugins/CodeHighlightShikiPlugin/index.js +14 -0
  228. package/lib/plugins/CollapsiblePlugin/CollapsibleContainerNode.d.ts +25 -0
  229. package/lib/plugins/CollapsiblePlugin/CollapsibleContainerNode.js +135 -0
  230. package/lib/plugins/CollapsiblePlugin/CollapsibleContentNode.d.ts +16 -0
  231. package/lib/plugins/CollapsiblePlugin/CollapsibleContentNode.js +83 -0
  232. package/lib/plugins/CollapsiblePlugin/CollapsibleTitleNode.d.ts +16 -0
  233. package/lib/plugins/CollapsiblePlugin/CollapsibleTitleNode.js +85 -0
  234. package/lib/plugins/CollapsiblePlugin/CollapsibleUtils.d.ts +2 -0
  235. package/lib/plugins/CollapsiblePlugin/CollapsibleUtils.js +12 -0
  236. package/lib/plugins/CollapsiblePlugin/index.d.ts +3 -0
  237. package/lib/plugins/CollapsiblePlugin/index.js +132 -0
  238. package/lib/plugins/CommentPlugin/index.d.ts +9 -0
  239. package/lib/plugins/CommentPlugin/index.js +460 -0
  240. package/lib/plugins/ComponentPickerPlugin/index.d.ts +2 -0
  241. package/lib/plugins/ComponentPickerPlugin/index.js +276 -0
  242. package/lib/plugins/ContextMenuPlugin/index.d.ts +2 -0
  243. package/lib/plugins/ContextMenuPlugin/index.js +112 -0
  244. package/lib/plugins/CounterPlugin/index.d.ts +3 -0
  245. package/lib/plugins/CounterPlugin/index.js +24 -0
  246. package/lib/plugins/DateTimePlugin/index.d.ts +8 -0
  247. package/lib/plugins/DateTimePlugin/index.js +28 -0
  248. package/lib/plugins/DebugPlugin/index.d.ts +3 -0
  249. package/lib/plugins/DebugPlugin/index.js +275 -0
  250. package/lib/plugins/DocsPlugin/index.d.ts +2 -0
  251. package/lib/plugins/DocsPlugin/index.js +4 -0
  252. package/lib/plugins/DragDropPastePlugin/index.d.ts +1 -0
  253. package/lib/plugins/DragDropPastePlugin/index.js +37 -0
  254. package/lib/plugins/DraggableBlockPlugin/index.d.ts +12 -0
  255. package/lib/plugins/DraggableBlockPlugin/index.js +36 -0
  256. package/lib/plugins/EmojiPickerPlugin/index.d.ts +1 -0
  257. package/lib/plugins/EmojiPickerPlugin/index.js +84 -0
  258. package/lib/plugins/EmojisPlugin/index.d.ts +2 -0
  259. package/lib/plugins/EmojisPlugin/index.js +56 -0
  260. package/lib/plugins/EquationsPlugin/index.d.ts +14 -0
  261. package/lib/plugins/EquationsPlugin/index.js +34 -0
  262. package/lib/plugins/ExcalidrawPlugin/index.d.ts +5 -0
  263. package/lib/plugins/ExcalidrawPlugin/index.js +44 -0
  264. package/lib/plugins/FigmaPlugin/index.d.ts +4 -0
  265. package/lib/plugins/FigmaPlugin/index.js +24 -0
  266. package/lib/plugins/FloatingLinkEditorPlugin/index.d.ts +15 -0
  267. package/lib/plugins/FloatingLinkEditorPlugin/index.js +280 -0
  268. package/lib/plugins/FloatingTextFormatToolbarPlugin/index.d.ts +7 -0
  269. package/lib/plugins/FloatingTextFormatToolbarPlugin/index.js +219 -0
  270. package/lib/plugins/ImagesPlugin/index.d.ts +24 -0
  271. package/lib/plugins/ImagesPlugin/index.js +195 -0
  272. package/lib/plugins/InlineImagePlugin/index.d.ts +17 -0
  273. package/lib/plugins/InlineImagePlugin/index.js +180 -0
  274. package/lib/plugins/KeywordsPlugin/index.d.ts +2 -0
  275. package/lib/plugins/KeywordsPlugin/index.js +35 -0
  276. package/lib/plugins/LayoutPlugin/InsertLayoutDialog.d.ts +6 -0
  277. package/lib/plugins/LayoutPlugin/InsertLayoutDialog.js +21 -0
  278. package/lib/plugins/LayoutPlugin/LayoutPlugin.d.ts +7 -0
  279. package/lib/plugins/LayoutPlugin/LayoutPlugin.js +135 -0
  280. package/lib/plugins/LinkPlugin/index.d.ts +6 -0
  281. package/lib/plugins/LinkPlugin/index.js +11 -0
  282. package/lib/plugins/MarkdownShortcutPlugin/index.d.ts +2 -0
  283. package/lib/plugins/MarkdownShortcutPlugin/index.js +6 -0
  284. package/lib/plugins/MarkdownTransformers/index.d.ts +8 -0
  285. package/lib/plugins/MarkdownTransformers/index.js +238 -0
  286. package/lib/plugins/MaxLengthPlugin/index.d.ts +3 -0
  287. package/lib/plugins/MaxLengthPlugin/index.js +41 -0
  288. package/lib/plugins/MentionsPlugin/index.d.ts +2 -0
  289. package/lib/plugins/MentionsPlugin/index.js +564 -0
  290. package/lib/plugins/PageBreakPlugin/index.d.ts +4 -0
  291. package/lib/plugins/PageBreakPlugin/index.js +31 -0
  292. package/lib/plugins/PasteLogPlugin/index.d.ts +2 -0
  293. package/lib/plugins/PasteLogPlugin/index.js +27 -0
  294. package/lib/plugins/PollPlugin/index.d.ts +8 -0
  295. package/lib/plugins/PollPlugin/index.js +38 -0
  296. package/lib/plugins/ShortcutsPlugin/index.d.ts +6 -0
  297. package/lib/plugins/ShortcutsPlugin/index.js +116 -0
  298. package/lib/plugins/ShortcutsPlugin/shortcuts.d.ts +59 -0
  299. package/lib/plugins/ShortcutsPlugin/shortcuts.js +173 -0
  300. package/lib/plugins/SpecialTextPlugin/index.d.ts +2 -0
  301. package/lib/plugins/SpecialTextPlugin/index.js +50 -0
  302. package/lib/plugins/SpeechToTextPlugin/index.d.ts +5 -0
  303. package/lib/plugins/SpeechToTextPlugin/index.js +86 -0
  304. package/lib/plugins/StickyPlugin/index.d.ts +2 -0
  305. package/lib/plugins/StickyPlugin/index.js +16 -0
  306. package/lib/plugins/TabFocusPlugin/index.d.ts +1 -0
  307. package/lib/plugins/TabFocusPlugin/index.js +38 -0
  308. package/lib/plugins/TableActionMenuPlugin/index.d.ts +5 -0
  309. package/lib/plugins/TableActionMenuPlugin/index.js +492 -0
  310. package/lib/plugins/TableCellResizer/index.d.ts +3 -0
  311. package/lib/plugins/TableCellResizer/index.js +297 -0
  312. package/lib/plugins/TableHoverActionsPlugin/index.d.ts +4 -0
  313. package/lib/plugins/TableHoverActionsPlugin/index.js +188 -0
  314. package/lib/plugins/TableOfContentsPlugin/index.d.ts +2 -0
  315. package/lib/plugins/TableOfContentsPlugin/index.js +116 -0
  316. package/lib/plugins/TablePlugin.d.ts +31 -0
  317. package/lib/plugins/TablePlugin.js +63 -0
  318. package/lib/plugins/TestRecorderPlugin/index.d.ts +3 -0
  319. package/lib/plugins/TestRecorderPlugin/index.js +346 -0
  320. package/lib/plugins/ToolbarPlugin/fontSize.d.ts +9 -0
  321. package/lib/plugins/ToolbarPlugin/fontSize.js +84 -0
  322. package/lib/plugins/ToolbarPlugin/index.d.ts +9 -0
  323. package/lib/plugins/ToolbarPlugin/index.js +500 -0
  324. package/lib/plugins/ToolbarPlugin/utils.d.ts +26 -0
  325. package/lib/plugins/ToolbarPlugin/utils.js +247 -0
  326. package/lib/plugins/TreeViewPlugin/index.d.ts +2 -0
  327. package/lib/plugins/TreeViewPlugin/index.js +7 -0
  328. package/lib/plugins/TwitterPlugin/index.d.ts +4 -0
  329. package/lib/plugins/TwitterPlugin/index.js +24 -0
  330. package/lib/plugins/TypingPerfPlugin/index.d.ts +2 -0
  331. package/lib/plugins/TypingPerfPlugin/index.js +97 -0
  332. package/lib/plugins/YouTubePlugin/index.d.ts +4 -0
  333. package/lib/plugins/YouTubePlugin/index.js +24 -0
  334. package/lib/server/validation.d.ts +1 -0
  335. package/lib/server/validation.js +115 -0
  336. package/lib/setupEnv.d.ts +2 -0
  337. package/lib/setupEnv.js +29 -0
  338. package/lib/themes/CommentEditorTheme.d.ts +4 -0
  339. package/lib/themes/CommentEditorTheme.js +11 -0
  340. package/lib/themes/PlaygroundEditorTheme.d.ts +4 -0
  341. package/lib/themes/PlaygroundEditorTheme.js +124 -0
  342. package/lib/themes/StickyEditorTheme.d.ts +4 -0
  343. package/lib/themes/StickyEditorTheme.js +11 -0
  344. package/lib/tyes.dt.d.ts +12 -0
  345. package/lib/tyes.dt.js +4 -0
  346. package/lib/ui/Button.d.ts +12 -0
  347. package/lib/ui/Button.js +6 -0
  348. package/lib/ui/ColorPicker.d.ts +14 -0
  349. package/lib/ui/ColorPicker.js +219 -0
  350. package/lib/ui/ContentEditable.d.ts +9 -0
  351. package/lib/ui/ContentEditable.js +6 -0
  352. package/lib/ui/Dialog.d.ts +10 -0
  353. package/lib/ui/Dialog.js +8 -0
  354. package/lib/ui/DropDown.d.ts +18 -0
  355. package/lib/ui/DropDown.js +133 -0
  356. package/lib/ui/DropdownColorPicker.d.ts +13 -0
  357. package/lib/ui/DropdownColorPicker.js +6 -0
  358. package/lib/ui/EquationEditor.d.ts +8 -0
  359. package/lib/ui/EquationEditor.js +11 -0
  360. package/lib/ui/ExcalidrawModal.d.ts +42 -0
  361. package/lib/ui/ExcalidrawModal.js +103 -0
  362. package/lib/ui/FileInput.d.ts +10 -0
  363. package/lib/ui/FileInput.js +5 -0
  364. package/lib/ui/FlashMessage.d.ts +7 -0
  365. package/lib/ui/FlashMessage.js +6 -0
  366. package/lib/ui/ImageResizer.d.ts +17 -0
  367. package/lib/ui/ImageResizer.js +171 -0
  368. package/lib/ui/KatexEquationAlterer.d.ts +8 -0
  369. package/lib/ui/KatexEquationAlterer.js +23 -0
  370. package/lib/ui/KatexRenderer.d.ts +6 -0
  371. package/lib/ui/KatexRenderer.js +24 -0
  372. package/lib/ui/Modal.d.ts +9 -0
  373. package/lib/ui/Modal.js +48 -0
  374. package/lib/ui/Select.d.ts +8 -0
  375. package/lib/ui/Select.js +5 -0
  376. package/lib/ui/Switch.d.ts +8 -0
  377. package/lib/ui/Switch.js +6 -0
  378. package/lib/ui/TextInput.d.ts +13 -0
  379. package/lib/ui/TextInput.js +7 -0
  380. package/lib/utils/docSerialization.d.ts +3 -0
  381. package/lib/utils/docSerialization.js +60 -0
  382. package/lib/utils/emoji-list.d.ts +20 -0
  383. package/lib/utils/emoji-list.js +16609 -0
  384. package/lib/utils/getDOMRangeRect.d.ts +8 -0
  385. package/lib/utils/getDOMRangeRect.js +26 -0
  386. package/lib/utils/getSelectedNode.d.ts +2 -0
  387. package/lib/utils/getSelectedNode.js +28 -0
  388. package/lib/utils/getThemeSelector.d.ts +2 -0
  389. package/lib/utils/getThemeSelector.js +14 -0
  390. package/lib/utils/isMobileWidth.d.ts +7 -0
  391. package/lib/utils/isMobileWidth.js +11 -0
  392. package/lib/utils/joinClasses.d.ts +1 -0
  393. package/lib/utils/joinClasses.js +7 -0
  394. package/lib/utils/setFloatingElemPosition.d.ts +1 -0
  395. package/lib/utils/setFloatingElemPosition.js +59 -0
  396. package/lib/utils/setFloatingElemPositionForLinkEditor.d.ts +1 -0
  397. package/lib/utils/setFloatingElemPositionForLinkEditor.js +36 -0
  398. package/lib/utils/swipe.d.ts +4 -0
  399. package/lib/utils/swipe.js +94 -0
  400. package/lib/utils/url.d.ts +2 -0
  401. package/lib/utils/url.js +31 -0
  402. package/package.json +4 -4
@@ -0,0 +1,946 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the MIT License.
4
+ */
5
+ import { EphemeralStore } from 'loro-crdt';
6
+ import { ObservableV2 } from 'lib0/observable';
7
+ import * as bc from 'lib0/broadcastchannel';
8
+ import * as time from 'lib0/time';
9
+ import * as math from 'lib0/math';
10
+ import * as url from 'lib0/url';
11
+ import * as env from 'lib0/environment';
12
+ import { generateClientID, generateRandomClientID } from '../utils/Utils';
13
+ // @todo - this should depend on ephemeral timeout
14
+ const messageReconnectTimeoutMs = 30 * 1000; // 30 seconds
15
+ // Loro message types
16
+ export const messageUpdate = 'update';
17
+ export const messageQuerySnapshot = 'query-snapshot';
18
+ export const messageEphemeral = 'ephemeral';
19
+ export const messageQueryEphemeral = 'query-ephemeral';
20
+ /**
21
+ * Awareness adapter that wraps EphemeralStore to provide awareness-like API
22
+ */
23
+ class AwarenessAdapter {
24
+ ephemeralStore;
25
+ localClientId;
26
+ eventHandlers = new Map();
27
+ constructor(ephemeralStore, doc) {
28
+ this.ephemeralStore = ephemeralStore;
29
+ // Use the same client ID as the binding for consistency
30
+ this.localClientId = doc ? generateClientID(doc) : generateRandomClientID();
31
+ console.log(' AwarenessAdapter created:', {
32
+ localClientId: this.localClientId,
33
+ docPeerId: doc ? doc.peerId : 'no-doc',
34
+ existingStatesCount: Object.keys(ephemeralStore.getAllStates()).length
35
+ });
36
+ // Subscribe to ephemeral store changes and emit awareness updates
37
+ this.ephemeralStore.subscribe((event) => {
38
+ // Emit update events when ephemeral state changes
39
+ const updateHandlers = this.eventHandlers.get('update') || [];
40
+ updateHandlers.forEach(integrater => integrater());
41
+ });
42
+ }
43
+ getLocalState() {
44
+ const localKey = this.localClientId.toString();
45
+ try {
46
+ const state = this.ephemeralStore.get(localKey);
47
+ return state ? state : null;
48
+ }
49
+ catch (error) {
50
+ console.warn(`[Client] AwarenessAdapter.getLocalState() - ephemeralStore.get() FAILED:`, {
51
+ error: error.message,
52
+ stack: error.stack,
53
+ localKey,
54
+ peerId: this.localClientId,
55
+ storeExists: !!this.ephemeralStore
56
+ });
57
+ throw error;
58
+ }
59
+ }
60
+ getStates() {
61
+ const states = new Map();
62
+ try {
63
+ // Get all states from ephemeral store
64
+ const allStates = this.ephemeralStore.getAllStates();
65
+ // Clean up stale states periodically (very rarely to avoid performance issues)
66
+ if (Math.random() < 0.001) { // ~0.1% chance per call
67
+ this.cleanupStaleStates(allStates);
68
+ }
69
+ // Iterate through all keys and extract user states
70
+ for (const [key, value] of Object.entries(allStates)) {
71
+ // Keys are now direct client ID strings
72
+ const clientId = parseInt(key, 10);
73
+ if (!isNaN(clientId) && value) {
74
+ states.set(clientId, value);
75
+ }
76
+ }
77
+ }
78
+ catch (error) {
79
+ console.warn(`[Client] AwarenessAdapter.getStates() - ERROR:`, error.message);
80
+ // Fallback to just local state
81
+ const localState = this.getLocalState();
82
+ if (localState) {
83
+ states.set(this.localClientId, localState);
84
+ }
85
+ }
86
+ return states;
87
+ }
88
+ cleanupStaleStates(allStates) {
89
+ const currentTime = Date.now();
90
+ const staleThreshold = 5 * 60 * 1000; // 5 minutes
91
+ try {
92
+ for (const [key, value] of Object.entries(allStates)) {
93
+ // Keys are now direct client ID strings
94
+ const clientId = parseInt(key, 10);
95
+ if (!isNaN(clientId) && value && typeof value === 'object') {
96
+ const state = value;
97
+ const lastActivity = typeof state.lastActivity === 'number' ? state.lastActivity : 0;
98
+ // Remove states that haven't been active for more than the threshold
99
+ if (typeof lastActivity === 'number' && currentTime - lastActivity > staleThreshold) {
100
+ console.log('Cleaning up stale user state:', key, 'last activity:', new Date(lastActivity).toISOString());
101
+ this.ephemeralStore.delete(key);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ catch (error) {
107
+ console.warn('Error during stale state cleanup:', error.message);
108
+ }
109
+ }
110
+ setLocalState(state) {
111
+ const localKey = this.localClientId.toString();
112
+ // Add lastActivity timestamp for stale state cleanup
113
+ const stateWithActivity = {
114
+ ...state,
115
+ lastActivity: Date.now()
116
+ };
117
+ try {
118
+ this.ephemeralStore.set(localKey, stateWithActivity);
119
+ }
120
+ catch (error) {
121
+ console.warn(`[Client] AwarenessAdapter.setLocalState() - ephemeralStore.set() FAILED:`, {
122
+ error: error.message,
123
+ stack: error.stack,
124
+ localKey,
125
+ peerId: this.localClientId,
126
+ stateKeys: Object.keys(state || {}),
127
+ storeExists: !!this.ephemeralStore
128
+ });
129
+ throw error;
130
+ }
131
+ }
132
+ setLocalStateField(field, value) {
133
+ const localState = this.getLocalState() || {
134
+ anchorPos: null,
135
+ awarenessData: {},
136
+ color: '#000000',
137
+ focusPos: null,
138
+ focusing: false,
139
+ name: 'Anonymous',
140
+ };
141
+ localState[field] = value;
142
+ this.setLocalState(localState);
143
+ }
144
+ on(type, cb) {
145
+ if (!this.eventHandlers.has(type)) {
146
+ this.eventHandlers.set(type, []);
147
+ }
148
+ this.eventHandlers.get(type).push(cb);
149
+ }
150
+ off(type, cb) {
151
+ const integrators = this.eventHandlers.get(type);
152
+ if (integrators) {
153
+ const index = integrators.indexOf(cb);
154
+ if (index !== -1) {
155
+ integrators.splice(index, 1);
156
+ }
157
+ }
158
+ }
159
+ // Manual cleanup method for debugging
160
+ forceCleanupStaleStates() {
161
+ try {
162
+ const allStates = this.ephemeralStore.getAllStates();
163
+ console.log('Force cleanup - total keys before:', Object.keys(allStates).length);
164
+ this.cleanupStaleStates(allStates);
165
+ const newStates = this.ephemeralStore.getAllStates();
166
+ console.log('Force cleanup - total keys after:', Object.keys(newStates).length);
167
+ }
168
+ catch (error) {
169
+ console.warn('Force cleanup failed:', error.message);
170
+ }
171
+ }
172
+ }
173
+ /**
174
+ * Message integrators for different Loro message types
175
+ */
176
+ const messageHandlers = {};
177
+ messageHandlers[messageQueryEphemeral] = (provider, message, _emitSynced) => {
178
+ try {
179
+ // Use encodeAll() to encode all ephemeral store data
180
+ const encodedData = provider.ephemeralStore.encodeAll();
181
+ const response = {
182
+ type: 'ephemeral',
183
+ ephemeral: Array.from(encodedData),
184
+ docId: message.docId
185
+ };
186
+ return JSON.stringify(response);
187
+ }
188
+ catch (error) {
189
+ console.warn('Error in messageQueryEphemeral integrater:', error.message);
190
+ return null;
191
+ }
192
+ };
193
+ messageHandlers[messageEphemeral] = (provider, message, _emitSynced) => {
194
+ try {
195
+ // Validate message data before processing
196
+ if (!message.ephemeral || message.ephemeral.length === 0) {
197
+ console.warn(`[Client] messageHandlers[messageEphemeral] - Skipping empty ephemeral data`);
198
+ return null;
199
+ }
200
+ // Reject obviously corrupted data (too small, common corrupt patterns)
201
+ if (message.ephemeral.length < 8) {
202
+ console.warn(`[Client] messageHandlers[messageEphemeral] - Rejecting suspiciously small ephemeral data:`, message.ephemeral.length);
203
+ return null;
204
+ }
205
+ // Additional validation - check for specific known bad patterns
206
+ if (message.ephemeral.length === 1) {
207
+ console.warn(`[Client] messageHandlers[messageEphemeral] - Rejecting single-byte ephemeral data (likely corrupted):`, message.ephemeral);
208
+ return null;
209
+ }
210
+ // Validate ephemeral data format by checking if it starts with reasonable values
211
+ // Ephemeral data should have a structured format - single random bytes are invalid
212
+ const firstBytes = message.ephemeral.slice(0, 4);
213
+ if (firstBytes.every(b => b === 0) || firstBytes.every(b => b === 255)) {
214
+ console.warn(`[Client] messageHandlers[messageEphemeral] - Rejecting ephemeral data with suspicious pattern:`, firstBytes);
215
+ return null;
216
+ }
217
+ // Apply ephemeral update
218
+ const ephemeralBytes = new Uint8Array(message.ephemeral);
219
+ // Use a try-catch specifically for the apply operation to isolate WASM errors
220
+ try {
221
+ provider.ephemeralStore.apply(ephemeralBytes);
222
+ }
223
+ catch (applyError) {
224
+ console.warn(`[Client] messageHandlers[messageEphemeral] - WASM apply() failed:`, {
225
+ error: applyError.message,
226
+ ephemeralLength: ephemeralBytes.length,
227
+ ephemeralSample: Array.from(ephemeralBytes.slice(0, 20))
228
+ });
229
+ // If this is a WASM memory error, don't attempt any more ephemeral operations
230
+ if (applyError.message && applyError.message.includes('memory access out of bounds')) {
231
+ console.warn(`[Client] messageHandlers[messageEphemeral] - CRITICAL: WASM memory corruption in apply(). Stopping ephemeral processing.`);
232
+ return null;
233
+ }
234
+ throw applyError; // Re-throw if not a WASM error
235
+ }
236
+ return null;
237
+ }
238
+ catch (error) {
239
+ console.warn(`[Client] messageHandlers[messageEphemeral] - ERROR in ephemeral message integrater:`, {
240
+ error: error.message,
241
+ stack: error.stack,
242
+ messageLength: message.ephemeral?.length,
243
+ ephemeralSample: message.ephemeral?.slice(0, 10)
244
+ });
245
+ return null;
246
+ }
247
+ };
248
+ messageHandlers[messageUpdate] = (provider, message, emitSynced) => {
249
+ try {
250
+ // Apply the update to the local document
251
+ const updateBytes = new Uint8Array(message.update);
252
+ // Get document state before applying update for comparison
253
+ // const beforeVersion = provider.doc.version()
254
+ // Import with sender's peerId as origin to mark as remote update
255
+ // We don't know the actual sender's peerId, so use a generic remote identifier
256
+ // The key point is that it's NOT our local peerId
257
+ const importStatus = provider.doc.import(updateBytes);
258
+ const afterVersion = provider.doc.version();
259
+ console.log('[SEED-DEBUG] messageUpdate: imported', updateBytes.length, 'bytes; importStatus=', JSON.stringify(importStatus));
260
+ // Update our last exported version to include the remote changes
261
+ // This ensures we don't re-export remote changes
262
+ provider._lastExportedVersion = afterVersion;
263
+ if (emitSynced && !provider._synced) {
264
+ provider.synced = true;
265
+ }
266
+ return null; // No response needed
267
+ }
268
+ catch (error) {
269
+ console.warn(` [LORO-UPDATE-ERROR] Failed to apply Loro update:`, error);
270
+ return null;
271
+ }
272
+ };
273
+ /**
274
+ * @param {WebsocketProvider} provider
275
+ * @param {string} reason
276
+ */
277
+ const permissionDeniedIntegrator = (provider, reason) => console.warn(`Permission denied to access ${provider.url}.\n${reason}`);
278
+ /**
279
+ * Process incoming message (JSON or binary) and return optional response
280
+ */
281
+ const processMessage = (provider, data, emitSynced) => {
282
+ if (data instanceof ArrayBuffer) {
283
+ try {
284
+ /*
285
+ // Try to decode as UTF-8 string (JSON messages sent as binary)
286
+ const decoder = new TextDecoder()
287
+ const jsonString = decoder.decode(data)
288
+ const message = JSON.parse(jsonString) as LoroWebSocketMessage
289
+ const messageHandler = messageHandlers[message.type]
290
+ if (messageHandler) {
291
+ return messageHandler(provider, message, emitSynced)
292
+ } else {
293
+ console.warn('Unknown message type:', message.type)
294
+ return null
295
+ }
296
+ */
297
+ // If JSON parsing fails, treat as raw binary Loro update
298
+ const updateBytes = new Uint8Array(data);
299
+ provider.doc.import(updateBytes);
300
+ if (emitSynced && !provider._synced) {
301
+ provider.synced = true;
302
+ }
303
+ return null; // No response needed for binary updates
304
+ }
305
+ catch (error) {
306
+ console.warn('Failed to process binary Loro update:', error);
307
+ return null;
308
+ }
309
+ }
310
+ else if (typeof data === 'string') {
311
+ try {
312
+ const message = JSON.parse(data);
313
+ const messageHandler = messageHandlers[message.type];
314
+ if (messageHandler) {
315
+ return messageHandler(provider, message, emitSynced);
316
+ }
317
+ else {
318
+ console.warn('Unknown message type:', message.type);
319
+ return null;
320
+ }
321
+ }
322
+ catch (error) {
323
+ console.warn('Failed to process JSON message:', error);
324
+ return null;
325
+ }
326
+ }
327
+ else if (typeof data === 'object' && data !== null) {
328
+ try {
329
+ const message = data;
330
+ const messageHandler = messageHandlers[message.type];
331
+ if (messageHandler) {
332
+ return messageHandler(provider, message, emitSynced);
333
+ }
334
+ else {
335
+ console.warn('Unknown message type:', message.type);
336
+ return null;
337
+ }
338
+ }
339
+ catch (error) {
340
+ console.warn('Failed to process object message:', error);
341
+ return null;
342
+ }
343
+ }
344
+ console.warn('Unknown message format:', typeof data);
345
+ return null;
346
+ };
347
+ /**
348
+ * Outsource this function so that a new websocket connection is created immediately.
349
+ * I suspect that the `ws.onclose` event is not always fired if there are network issues.
350
+ *
351
+ * @param {WebsocketProvider} provider
352
+ * @param {WebSocket} ws
353
+ * @param {CloseEvent | null} event
354
+ */
355
+ const closeWebsocketConnection = (provider, ws, event) => {
356
+ if (ws === provider.ws) {
357
+ provider.emit('connection-close', [event, provider]);
358
+ provider.ws = null;
359
+ ws.close();
360
+ provider.wsconnecting = false;
361
+ if (provider.wsconnected) {
362
+ provider.wsconnected = false;
363
+ provider.synced = false;
364
+ // Clear local ephemeral state on disconnect
365
+ provider.ephemeralStore.delete('presence');
366
+ provider.ephemeralStore.delete('cursor');
367
+ // Clear user-specific state
368
+ try {
369
+ const peerId = generateClientID(provider.doc);
370
+ const userKey = peerId.toString();
371
+ provider.ephemeralStore.delete(userKey);
372
+ console.log('Disconnect cleanup: removed user key:', userKey);
373
+ }
374
+ catch (error) {
375
+ console.warn('Disconnect cleanup failed:', error.message);
376
+ }
377
+ provider.emit('status', [{
378
+ status: 'disconnected'
379
+ }]);
380
+ }
381
+ else {
382
+ provider.wsUnsuccessfulReconnects++;
383
+ }
384
+ // Start with no reconnect timeout and increase timeout by
385
+ // using exponential backoff starting with 100ms
386
+ setTimeout(setupWS, math.min(math.pow(2, provider.wsUnsuccessfulReconnects) * 100, provider.maxBackoffTime), provider);
387
+ }
388
+ };
389
+ const sendMessage = (ws, message) => {
390
+ if (ws && ws.readyState === ws.OPEN) {
391
+ try {
392
+ const m = JSON.stringify(message);
393
+ ws.send(m);
394
+ }
395
+ catch (error) {
396
+ console.warn('Failed to send message over WebSocket:', error);
397
+ }
398
+ }
399
+ else {
400
+ console.warn('WebSocket not open, cannot send message');
401
+ }
402
+ };
403
+ /**
404
+ * @param {WebsocketProvider} provider
405
+ */
406
+ const setupWS = (provider) => {
407
+ if (provider.shouldConnect && provider.ws === null) {
408
+ const ws = new provider._WS(provider.url, provider.protocols);
409
+ ws.binaryType = 'arraybuffer';
410
+ provider.ws = ws;
411
+ provider.wsconnecting = true;
412
+ provider.wsconnected = false;
413
+ provider.synced = false;
414
+ ws.onmessage = (event) => {
415
+ provider.wsLastMessageReceived = time.getUnixTime();
416
+ const response = processMessage(provider, event.data, true);
417
+ if (response) {
418
+ // TODO
419
+ // sendMessage(ws, response)
420
+ }
421
+ };
422
+ ws.onerror = (event) => {
423
+ provider.emit('connection-error', [event, provider]);
424
+ };
425
+ ws.onclose = (event) => {
426
+ closeWebsocketConnection(provider, ws, event);
427
+ };
428
+ ws.onopen = () => {
429
+ provider.wsLastMessageReceived = time.getUnixTime();
430
+ provider.wsconnecting = false;
431
+ provider.wsconnected = true;
432
+ provider.wsUnsuccessfulReconnects = 0;
433
+ provider.emit('status', [{
434
+ status: 'connected'
435
+ }]);
436
+ console.log(' WebSocket connection established, requesting initial data');
437
+ // Since we're in onopen, we know the WebSocket is ready
438
+ // Use sendMessage directly to avoid any race conditions
439
+ // Only request snapshot if we haven't already loaded it
440
+ if (!provider.snapshotLoaded) {
441
+ // First request a snapshot to get the initial document state
442
+ const requestId = Math.random().toString(36).substr(2, 9);
443
+ const clientId = generateClientID(provider.doc).toString();
444
+ const snapshotRequest = {
445
+ type: 'query-snapshot',
446
+ docId: provider.docId,
447
+ clientId: clientId
448
+ };
449
+ console.log(` Requesting initial snapshot from server (ID: ${requestId}, clientId: ${clientId}):`, snapshotRequest);
450
+ console.log(` Provider instance ID: ${provider.wsServerUrl}/${provider.docId}, snapshotLoaded: ${provider.snapshotLoaded}`);
451
+ sendMessage(ws, snapshotRequest);
452
+ }
453
+ else {
454
+ console.log(' Snapshot already loaded, skipping request');
455
+ }
456
+ // Then request initial ephemeral state from server
457
+ const clientId = generateClientID(provider.doc).toString();
458
+ const ephemeralRequest = {
459
+ type: 'query-ephemeral',
460
+ docId: provider.docId,
461
+ clientId: clientId
462
+ };
463
+ sendMessage(ws, ephemeralRequest);
464
+ // broadcast local ephemeral state if any
465
+ const localState = provider.ephemeralStore.getAllStates();
466
+ if (Object.keys(localState).length > 0) {
467
+ try {
468
+ // Use encodeAll() to encode all ephemeral store data
469
+ const encodedData = provider.ephemeralStore.encodeAll();
470
+ const ephemeralMessage = {
471
+ type: 'ephemeral',
472
+ ephemeral: Array.from(encodedData),
473
+ docId: provider.docId
474
+ };
475
+ sendMessage(ws, ephemeralMessage);
476
+ }
477
+ catch (error) {
478
+ console.warn(`[Client] setupWS - MAJOR ERROR in ephemeral process:`, {
479
+ error: error.message,
480
+ stack: error.stack,
481
+ localStateKeys: Object.keys(localState),
482
+ storeExists: !!provider.ephemeralStore
483
+ });
484
+ }
485
+ }
486
+ // Flush any local document updates that were buffered while the socket
487
+ // was not ready to send (e.g. the initial-content bootstrap ran before
488
+ // the connection opened). Without this, seeded content committed before
489
+ // the socket opened would never reach the server or other collaborators.
490
+ if (provider._pendingUpdates.length > 0) {
491
+ const pending = provider._pendingUpdates;
492
+ provider._pendingUpdates = [];
493
+ console.log('[SEED-DEBUG] ws.onopen: flushing', pending.length, 'buffered local updates');
494
+ for (const pendingUpdate of pending) {
495
+ sendMessage(ws, {
496
+ type: 'update',
497
+ update: Array.from(pendingUpdate),
498
+ docId: provider.docId
499
+ });
500
+ }
501
+ }
502
+ };
503
+ provider.emit('status', [{
504
+ status: 'connecting'
505
+ }]);
506
+ }
507
+ };
508
+ /**
509
+ * Broadcast JSON message to WebSocket and BroadcastChannel
510
+ */
511
+ const broadcastMessage = (provider, message) => {
512
+ const ws = provider.ws;
513
+ if (provider.wsconnected && ws && ws.readyState === ws.OPEN) {
514
+ sendMessage(ws, message);
515
+ }
516
+ else {
517
+ console.log(' [BROADCAST] WebSocket not ready for sending');
518
+ }
519
+ if (provider.bcconnected) {
520
+ bc.publish(provider.bcChannel, JSON.stringify(message), provider);
521
+ }
522
+ else {
523
+ console.log(' [BROADCAST] BroadcastChannel not connected');
524
+ }
525
+ };
526
+ /**
527
+ * Websocket Provider for Loro. Creates a websocket connection to sync the shared document.
528
+ * The document name is attached to the provided url. I.e. the following example
529
+ * creates a websocket connection to http://localhost:1235/my-document-name
530
+ */
531
+ export class WebsocketProvider extends ObservableV2 {
532
+ static globalEphemeralStore = null;
533
+ wsServerUrl = '';
534
+ docId = '';
535
+ doc = null;
536
+ _WS = null;
537
+ protocols = [];
538
+ params = {};
539
+ ephemeralStore = null;
540
+ awareness = null;
541
+ ws = null;
542
+ wsconnected = false;
543
+ wsconnecting = false;
544
+ bcconnected = false;
545
+ disableBc = false;
546
+ bcChannel = '';
547
+ maxBackoffTime = 2500;
548
+ wsUnsuccessfulReconnects = 0;
549
+ messageHandlers = [];
550
+ _synced = false;
551
+ wsLastMessageReceived = 0;
552
+ shouldConnect = false;
553
+ snapshotLoaded = false;
554
+ _checkInterval = null;
555
+ _resyncInterval = null;
556
+ _updateHandler = null;
557
+ _ephemeralUpdateIntegrator = null;
558
+ _exitIntegrator = null;
559
+ _bcSubscriber = null;
560
+ _lastExportedVersion = null; // Track last exported version for incremental updates
561
+ _pendingUpdates = []; // Local updates buffered while the socket is not ready to send
562
+ /**
563
+ * @param {string} wsServerUrl
564
+ * @param {string} docId
565
+ * @param {LoroDoc} doc
566
+ * @param {object} opts
567
+ * @param {boolean} [opts.connect]
568
+ * @param {EphemeralStore} [opts.ephemeralStore]
569
+ * @param {Object<string,string>} [opts.params] specify url parameters
570
+ * @param {Array<string>} [opts.protocols] specify websocket protocols
571
+ * @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionally provide a WebSocket polyfill
572
+ * @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds
573
+ * @param {number} [opts.maxBackoffTime] Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff)
574
+ * @param {boolean} [opts.disableBc] Disable cross-tab BroadcastChannel communication
575
+ */
576
+ constructor(wsServerUrl, docId, doc, { connect = true, ephemeralStore = undefined, params = {}, protocols = [], WebSocketPolyfill = WebSocket, resyncInterval = -1, maxBackoffTime = 2500, disableBc = false } = {}) {
577
+ super();
578
+ // ensure that serverUrl does not end with /
579
+ while (wsServerUrl[wsServerUrl.length - 1] === '/') {
580
+ wsServerUrl = wsServerUrl.slice(0, wsServerUrl.length - 1);
581
+ }
582
+ this.wsServerUrl = wsServerUrl;
583
+ this.bcChannel = wsServerUrl + '/' + docId;
584
+ this.maxBackoffTime = maxBackoffTime;
585
+ /**
586
+ * The specified url parameters. This can be safely updated. The changed parameters will be used
587
+ * when a new connection is established.
588
+ * @type {Object<string,string>}
589
+ */
590
+ this.params = params;
591
+ this.protocols = protocols;
592
+ this.docId = docId;
593
+ this.doc = doc;
594
+ this._WS = WebSocketPolyfill;
595
+ // Create or reuse persistent ephemeral store for the entire user session
596
+ try {
597
+ if (ephemeralStore) {
598
+ // Use provided ephemeral store (already persistent)
599
+ this.ephemeralStore = ephemeralStore;
600
+ }
601
+ else {
602
+ // Create or reuse global ephemeral store for session persistence
603
+ if (!WebsocketProvider.globalEphemeralStore) {
604
+ WebsocketProvider.globalEphemeralStore = new EphemeralStore(300000); // 5 minute timeout
605
+ console.log('🆕 Created new global EphemeralStore');
606
+ }
607
+ else {
608
+ console.log(' Reusing existing global EphemeralStore - cleaning up stale user states');
609
+ // Clean up all existing user states when reusing store to prevent accumulation
610
+ const allStates = WebsocketProvider.globalEphemeralStore.getAllStates();
611
+ Object.keys(allStates).forEach(key => {
612
+ // Keys are now direct client ID strings, check if it's a valid client ID
613
+ const clientId = parseInt(key, 10);
614
+ if (!isNaN(clientId)) {
615
+ WebsocketProvider.globalEphemeralStore.delete(key);
616
+ console.log(' Cleaned up stale user state:', key);
617
+ }
618
+ });
619
+ }
620
+ this.ephemeralStore = WebsocketProvider.globalEphemeralStore;
621
+ }
622
+ }
623
+ catch (error) {
624
+ console.warn(`[Client] WebsocketProvider constructor - ERROR setting up EphemeralStore:`, {
625
+ error: error.message,
626
+ stack: error.stack,
627
+ docId
628
+ });
629
+ throw error;
630
+ }
631
+ // Create awareness adapter that wraps ephemeral store
632
+ this.awareness = new AwarenessAdapter(this.ephemeralStore, this.doc);
633
+ this.wsconnected = false;
634
+ this.wsconnecting = false;
635
+ this.bcconnected = false;
636
+ this.disableBc = disableBc;
637
+ this.wsUnsuccessfulReconnects = 0;
638
+ /**
639
+ * @type {boolean}
640
+ */
641
+ this._synced = false;
642
+ /**
643
+ * @type {WebSocket?}
644
+ */
645
+ this.ws = null;
646
+ this.wsLastMessageReceived = 0;
647
+ /**
648
+ * Whether to connect to other peers or not
649
+ * @type {boolean}
650
+ */
651
+ this.shouldConnect = connect;
652
+ /**
653
+ * @type {number}
654
+ */
655
+ this._resyncInterval = 0;
656
+ if (resyncInterval > 0) {
657
+ this._resyncInterval = /** @type {any} */ (setInterval(() => {
658
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
659
+ // Request fresh ephemeral state from server with client ID
660
+ const clientId = generateClientID(this.doc).toString();
661
+ const queryMessage = {
662
+ type: 'query-ephemeral',
663
+ docId: this.docId,
664
+ clientId: clientId
665
+ };
666
+ sendMessage(this.ws, queryMessage);
667
+ }
668
+ }, resyncInterval));
669
+ }
670
+ /**
671
+ * @param {string | object} data
672
+ * @param {any} origin
673
+ */
674
+ this._bcSubscriber = (data, origin) => {
675
+ if (origin !== this) {
676
+ const response = processMessage(this, data, false);
677
+ if (response) {
678
+ bc.publish(this.bcChannel, response, this);
679
+ }
680
+ }
681
+ };
682
+ /**
683
+ * Listens to Loro Loro updates and sends them to remote peers (ws and broadcastchannel)
684
+ * @param {Uint8Array} update
685
+ * @param {any} origin
686
+ */
687
+ this._updateHandler = (update) => {
688
+ // This integrater is only called for local changes that need to be broadcast
689
+ const updateMessage = {
690
+ type: 'update',
691
+ update: Array.from(update),
692
+ docId: this.docId
693
+ };
694
+ const ws = this.ws;
695
+ if (this.wsconnected && ws && ws.readyState === ws.OPEN) {
696
+ console.log('[SEED-DEBUG] _updateHandler: sending local update over WS', update.length, 'bytes');
697
+ sendMessage(ws, updateMessage);
698
+ }
699
+ else {
700
+ // The socket is not ready yet — this happens when the initial-content
701
+ // bootstrap commits before the connection opens. Loro delivers each
702
+ // local update to subscribeLocalUpdates exactly once, so dropping it
703
+ // here would lose the seeded content permanently (remote peers would
704
+ // never receive it). Buffer it and flush on open instead.
705
+ console.log('[SEED-DEBUG] _updateHandler: WS not ready, buffering local update', update.length, 'bytes (pending now', this._pendingUpdates.length + 1, ')');
706
+ this._pendingUpdates.push(update);
707
+ }
708
+ // Mirror to other same-origin tabs when the BroadcastChannel is available.
709
+ if (this.bcconnected) {
710
+ bc.publish(this.bcChannel, JSON.stringify(updateMessage), this);
711
+ }
712
+ };
713
+ // Document update integrater - called when Loro emits document change events
714
+ /**
715
+ * @param {EphemeralStoreEvent} event - EphemeralStoreEvent with added, updated, removed arrays
716
+ */
717
+ this._ephemeralUpdateIntegrator = (event) => {
718
+ // Only broadcast if there are actual changes
719
+ if (event.added.length > 0 || event.updated.length > 0 || event.removed.length > 0) {
720
+ try {
721
+ // Use encodeAll() to encode all ephemeral store data
722
+ const encodedData = this.ephemeralStore.encodeAll();
723
+ const ephemeralMessage = {
724
+ type: 'ephemeral',
725
+ ephemeral: Array.from(encodedData),
726
+ docId: this.docId
727
+ };
728
+ broadcastMessage(this, ephemeralMessage);
729
+ }
730
+ catch (error) {
731
+ console.warn(`[Client] _ephemeralUpdateIntegrator - ERROR:`, error.message);
732
+ // Fallback: skip this update rather than crash
733
+ }
734
+ }
735
+ };
736
+ this._exitIntegrator = () => {
737
+ // Clear only our local ephemeral state on exit, don't destroy the global store
738
+ if (this.ephemeralStore && this.awareness) {
739
+ try {
740
+ const peerId = generateClientID(this.doc);
741
+ const userKey = peerId.toString();
742
+ this.ephemeralStore.delete(userKey);
743
+ }
744
+ catch (error) {
745
+ console.warn(`[Client] Process exit - Could not clear user state:`, error.message);
746
+ }
747
+ }
748
+ };
749
+ if (env.isNode && typeof process !== 'undefined') {
750
+ process.on('exit', this._exitIntegrator);
751
+ }
752
+ this.ephemeralStore.subscribe(this._ephemeralUpdateIntegrator);
753
+ // Initialize the last exported version to current document version
754
+ this._lastExportedVersion = this.doc.version();
755
+ // Use Loro's native event system to listen for document changes
756
+ try {
757
+ /*
758
+ this.doc.subscribe((event: LoroEventBatch) => {
759
+ try {
760
+ const afterCommitVersion = this.doc.version()
761
+ const update = this.doc.export({
762
+ mode: 'update',
763
+ from: this._lastExportedVersion
764
+ });
765
+
766
+ if (update.length > 0) {
767
+ this._updateHandler(update);
768
+ // Update the last exported version to current version
769
+ this._lastExportedVersion = afterCommitVersion
770
+ } else {
771
+ console.warn(`[WEBSOCKET-PROVIDER] No incremental update available - versions might be the same`);
772
+ }
773
+ } catch (error) {
774
+ console.warn(`[WEBSOCKET-PROVIDER] Error exporting incremental update:`, error);
775
+ }
776
+ });
777
+ */
778
+ this.doc.subscribeLocalUpdates((update) => {
779
+ try {
780
+ this._updateHandler(update);
781
+ }
782
+ catch (error) {
783
+ console.warn(`[WEBSOCKET-PROVIDER] Error exporting incremental update:`, error);
784
+ }
785
+ });
786
+ }
787
+ catch (error) {
788
+ console.warn(`[Client] ERROR setting up Loro document subscription:`, error);
789
+ }
790
+ this._checkInterval = (setInterval(() => {
791
+ if (this.wsconnected &&
792
+ messageReconnectTimeoutMs <
793
+ time.getUnixTime() - this.wsLastMessageReceived) {
794
+ // no message received in a long time - not even your own ephemeral
795
+ // updates (which are updated every 15 seconds)
796
+ closeWebsocketConnection(this, this.ws, null);
797
+ }
798
+ }, messageReconnectTimeoutMs / 10));
799
+ if (connect) {
800
+ this.connect();
801
+ }
802
+ }
803
+ get url() {
804
+ const encodedParams = url.encodeQueryParams(this.params);
805
+ return this.wsServerUrl + '/' + this.docId +
806
+ (encodedParams.length === 0 ? '' : '?' + encodedParams);
807
+ }
808
+ /**
809
+ * @type {boolean}
810
+ */
811
+ get synced() {
812
+ return this._synced;
813
+ }
814
+ set synced(state) {
815
+ if (this._synced !== state) {
816
+ this._synced = state;
817
+ super.emit('synced', [state]);
818
+ super.emit('sync', [state]);
819
+ }
820
+ }
821
+ destroy() {
822
+ if (this._resyncInterval !== 0) {
823
+ clearInterval(this._resyncInterval);
824
+ }
825
+ clearInterval(this._checkInterval);
826
+ this.disconnect();
827
+ if (env.isNode && typeof process !== 'undefined') {
828
+ process.off('exit', this._exitIntegrator);
829
+ }
830
+ // DON'T destroy the ephemeral store - it's shared across the session
831
+ // Only clear our local state from it
832
+ if (this.ephemeralStore && this.awareness) {
833
+ try {
834
+ const peerId = generateClientID(this.doc);
835
+ const userKey = peerId.toString();
836
+ this.ephemeralStore.delete(userKey);
837
+ }
838
+ catch (error) {
839
+ console.warn(`[Client] WebsocketProvider.destroy - Could not clear user state:`, error.message);
840
+ }
841
+ }
842
+ // Note: LoroDoc doesn't have event listeners to remove
843
+ super.destroy();
844
+ }
845
+ connectBc() {
846
+ if (this.disableBc) {
847
+ return;
848
+ }
849
+ if (!this.bcconnected) {
850
+ bc.subscribe(this.bcChannel, this._bcSubscriber);
851
+ this.bcconnected = true;
852
+ }
853
+ // Note: BroadcastChannel snapshot sharing removed - only WebSocket queries supported
854
+ // Query ephemeral state from other tabs
855
+ const clientId = generateClientID(this.doc).toString();
856
+ const queryMessage = {
857
+ type: 'query-ephemeral',
858
+ docId: this.docId,
859
+ clientId: clientId
860
+ };
861
+ bc.publish(this.bcChannel, JSON.stringify(queryMessage), this);
862
+ // Broadcast local ephemeral state using container approach
863
+ const localState = this.ephemeralStore.getAllStates();
864
+ if (Object.keys(localState).length > 0) {
865
+ try {
866
+ // Use encodeAll() to encode all ephemeral store data
867
+ const encodedData = this.ephemeralStore.encodeAll();
868
+ const ephemeralMessage = {
869
+ type: 'ephemeral',
870
+ ephemeral: Array.from(encodedData),
871
+ docId: this.docId
872
+ };
873
+ bc.publish(this.bcChannel, JSON.stringify(ephemeralMessage), this);
874
+ }
875
+ catch (error) {
876
+ console.warn('Error broadcasting ephemeral state in connectBc:', error.message);
877
+ }
878
+ }
879
+ }
880
+ disconnectBc() {
881
+ // broadcast message with local ephemeral state cleared (indicating disconnect)
882
+ this.ephemeralStore.delete('presence');
883
+ this.ephemeralStore.delete('cursor');
884
+ // Clear user-specific state
885
+ try {
886
+ const peerId = generateClientID(this.doc);
887
+ const userKey = peerId.toString();
888
+ this.ephemeralStore.delete(userKey);
889
+ console.log('Broadcast disconnect cleanup: removed user key:', userKey);
890
+ }
891
+ catch (error) {
892
+ console.warn('Broadcast disconnect cleanup failed:', error.message);
893
+ }
894
+ try {
895
+ // Use encodeAll() to encode ephemeral store data for disconnect broadcast
896
+ const encodedData = this.ephemeralStore.encodeAll();
897
+ const ephemeralMessage = {
898
+ type: 'ephemeral',
899
+ ephemeral: Array.from(encodedData),
900
+ docId: this.docId
901
+ };
902
+ broadcastMessage(this, ephemeralMessage);
903
+ }
904
+ catch (error) {
905
+ console.warn('Error broadcasting disconnect in disconnectBc:', error.message);
906
+ }
907
+ if (this.bcconnected) {
908
+ bc.unsubscribe(this.bcChannel, this._bcSubscriber);
909
+ this.bcconnected = false;
910
+ }
911
+ }
912
+ disconnect() {
913
+ this.shouldConnect = false;
914
+ this.disconnectBc();
915
+ if (this.ws !== null) {
916
+ closeWebsocketConnection(this, this.ws, null);
917
+ }
918
+ }
919
+ connect() {
920
+ this.shouldConnect = true;
921
+ if (!this.wsconnected && this.ws === null) {
922
+ setupWS(this);
923
+ this.connectBc();
924
+ }
925
+ }
926
+ /**
927
+ * Manually send a Loro document update to connected peers
928
+ * Call this method after making changes to the LoroDoc
929
+ * @param {Uint8Array} update The update bytes from LoroDoc
930
+ */
931
+ sendUpdate(update) {
932
+ this._updateHandler(update, null);
933
+ }
934
+ /**
935
+ * Force cleanup of stale ephemeral states (for debugging)
936
+ * Removes user states that haven't been active for more than 5 minutes
937
+ */
938
+ cleanupStaleStates() {
939
+ if (this.awareness && typeof this.awareness.forceCleanupStaleStates === 'function') {
940
+ this.awareness.forceCleanupStaleStates();
941
+ }
942
+ else {
943
+ console.warn('Cleanup method not available on awareness provider');
944
+ }
945
+ }
946
+ }