collabmd 0.1.44 → 0.1.46

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 (291) hide show
  1. package/README.md +39 -19
  2. package/bin/collabmd.js +27 -1
  3. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CYM5nxUi.js +1 -0
  4. package/dist/client/assets/browser-utils--H_DkyEF.js +1 -0
  5. package/dist/client/assets/chunk-ERSWUTIN-hHdvnhaY.js +41 -0
  6. package/dist/client/assets/{chunk-CZQZEIFA-DT3N9fo7.js → chunk-YGJVY4SA-CjSe7Bx7.js} +2 -2
  7. package/dist/client/assets/comment-thread-store-CdcwrObc.js +1 -0
  8. package/dist/client/assets/comment-threads-9J6Em-_y.js +4 -0
  9. package/dist/client/assets/comment-ui-shared-BC4pt4w8.js +7 -0
  10. package/dist/client/assets/dist-B5HTH2BO.js +1 -0
  11. package/dist/client/assets/{dist-DhLvC8DS.js → dist-B5Jf1SLZ.js} +2 -2
  12. package/dist/client/assets/{dist-CnL7Y2sM.js → dist-BDt3rz7l.js} +1 -1
  13. package/dist/client/assets/dist-BEwK4csK.js +1 -0
  14. package/dist/client/assets/dist-BKW4PALF.js +1 -0
  15. package/dist/client/assets/dist-BWcHHOx-.js +1 -0
  16. package/dist/client/assets/{dist-CYZmbokb.js → dist-BalScOwZ.js} +9 -9
  17. package/dist/client/assets/dist-Bf8f4HmM.js +6 -0
  18. package/dist/client/assets/dist-BjHJDtyU.js +1 -0
  19. package/dist/client/assets/dist-Bm6pf9Bl.js +1 -0
  20. package/dist/client/assets/dist-BoVBdSjk.js +1 -0
  21. package/dist/client/assets/dist-CC5IrQMG.js +1 -0
  22. package/dist/client/assets/{dist-C11hHqv_.js → dist-CCP_AKPG.js} +12 -12
  23. package/dist/client/assets/dist-CZgOmxK6.js +1 -0
  24. package/dist/client/assets/{dist-D2AQ-76g.js → dist-Cqhv5LAk.js} +1 -1
  25. package/dist/client/assets/{dist-MvcV4190.js → dist-D-Z6FLem.js} +1 -1
  26. package/dist/client/assets/dist-DLMiIsNC.js +11 -0
  27. package/dist/client/assets/{dist-3bgCp7iY.js → dist-DZKu9Cs8.js} +1 -1
  28. package/dist/client/assets/dist-DaYdhcVs.js +1 -0
  29. package/dist/client/assets/dist-FWJsgGYI.js +1 -0
  30. package/dist/client/assets/dist-Kh2COra_.js +1 -0
  31. package/dist/client/assets/{dist-JYEDRrPT.js → dist-PDZ3bSqm.js} +1 -1
  32. package/dist/client/assets/{dist-Drp8uu5J.js → dist-TjMh8n1u.js} +1 -1
  33. package/dist/client/assets/{dist-DGb27Dl6.js → dist-dtAdgL39.js} +1 -1
  34. package/dist/client/assets/dist-jv4WdPnK.js +9 -0
  35. package/dist/client/assets/{dist-DK5Gs2wa.js → dist-rGBmqxC7.js} +1 -1
  36. package/dist/client/assets/{dist-DCzuX1AF.js → dist-shrl6K_0.js} +1 -1
  37. package/dist/client/assets/dist-tOaegDYS2.js +1 -0
  38. package/dist/client/assets/dist-ule2n6ak.js +1 -0
  39. package/dist/client/assets/dist-xElaxbia.js +1 -0
  40. package/dist/client/assets/drawio-editor-FzY1cVw8.js +9 -0
  41. package/dist/client/assets/drawioEditor-BjiKJqlc.js +2 -0
  42. package/dist/client/assets/editor-session-Cm9ekeNo.js +54 -0
  43. package/dist/client/assets/embedded-editor-base-CwTkk911.css +1 -0
  44. package/dist/client/assets/{en-GADW4C6J-DQ6zyCEM.js → en-7NFCYGKX-BrgYZAEe.js} +1 -1
  45. package/dist/client/assets/{preview-render-profile-BZzGSg6K.js → es-DXGV3RHd.js} +13 -13
  46. package/dist/client/assets/excalidraw-editor-B46zsTFI.js +9 -0
  47. package/dist/client/assets/excalidraw-editor-Bt7gMSoh.css +1 -0
  48. package/dist/client/assets/excalidraw-scene-BOben2gr.js +1 -0
  49. package/dist/client/assets/excalidrawEditor-CT2pHq7e.js +2 -0
  50. package/dist/client/assets/export-page-A7NHawJY.js +39 -0
  51. package/dist/client/assets/exportDocument-B2L8fe34.css +1 -0
  52. package/dist/client/assets/exportDocument-BaqcOAAW.js +2 -0
  53. package/dist/client/assets/file-icon-svg-CQAjlWjK.js +1 -0
  54. package/dist/client/assets/file-kind-BbqgGzpc.js +1 -0
  55. package/dist/client/assets/image-QXRYHUNP-SWEV8H0J.js +1 -0
  56. package/dist/client/assets/index-ClbjMBVt.js +2 -0
  57. package/dist/client/assets/index-Pum8ET-5.css +1 -0
  58. package/dist/client/assets/main-B3uPyqic.js +893 -0
  59. package/dist/client/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
  60. package/dist/client/assets/pdf-BL9Pr2D5.js +54 -0
  61. package/dist/client/assets/pdf.worker-CLesOks4.mjs +64857 -0
  62. package/dist/client/assets/pdf_viewer-FVkkThnz.js +551 -0
  63. package/dist/client/assets/preview-diagram-utils-txBCoW8d.js +1 -0
  64. package/dist/client/assets/{preview-render-compiler-BSaQO_p3.js → preview-render-compiler-BD-XhkS2.js} +2 -2
  65. package/dist/client/assets/preview-render-compiler-BKedaMxx.js +1 -0
  66. package/dist/client/assets/preview-render-profile-CE-5rVTC.js +1 -0
  67. package/dist/client/assets/{preview-render-worker-B42LWvrD.js → preview-render-worker-CiPqc9n6.js} +20 -20
  68. package/dist/client/assets/prod-Dy20X7eT.js +1 -0
  69. package/dist/client/assets/prod-hnwh7wRM.js +177 -0
  70. package/dist/client/assets/quick-switcher-controller-fqOYR7qR.js +12 -0
  71. package/dist/client/assets/runtime-config-BLrEKVO1.js +1 -0
  72. package/dist/client/assets/vault-api-client-_LpV1aIn.js +6 -0
  73. package/dist/client/assets/{yjs-provider-reset-guard-Cox4nxwu.js → yjs-provider-reset-guard-DE0vINDP.js} +1 -1
  74. package/dist/client/drawio-editor.html +3 -2
  75. package/dist/client/excalidraw-editor.html +3 -2
  76. package/dist/client/export-document.html +4 -3
  77. package/dist/client/index.html +109 -27
  78. package/docker-compose.yml +42 -0
  79. package/package.json +26 -15
  80. package/scripts/local-structurizr-compose.mjs +101 -0
  81. package/src/client/app/drawio-editor-entry.js +0 -3
  82. package/src/client/app/drawio-editor.html +1 -0
  83. package/src/client/app/excalidraw-editor-entry.js +0 -3
  84. package/src/client/app/excalidraw-editor.html +1 -0
  85. package/src/client/app/export-document-entry.js +0 -3
  86. package/src/client/app/export-document.html +1 -0
  87. package/src/client/app/index.html +106 -24
  88. package/src/client/app/main-entry.js +0 -5
  89. package/src/client/application/app-shell/chat-feature.js +180 -124
  90. package/src/client/application/app-shell/comments-feature.js +20 -11
  91. package/src/client/application/app-shell/git-feature.js +16 -9
  92. package/src/client/application/app-shell/presence-feature.js +18 -11
  93. package/src/client/application/app-shell/ui-feature-shell.js +78 -17
  94. package/src/client/application/app-shell/ui-feature-sidebar.js +74 -1
  95. package/src/client/application/app-shell/ui-feature-tab-activity.js +2 -2
  96. package/src/client/application/app-shell/ui-feature-toolbar.js +2 -52
  97. package/src/client/application/app-shell/workspace-feature.js +27 -84
  98. package/src/client/application/app-shell-elements.js +9 -1
  99. package/src/client/application/diagram-chrome.js +161 -166
  100. package/src/client/application/diagram-preview-export.js +7 -34
  101. package/src/client/application/diagram-preview-hydrator.js +1 -19
  102. package/src/client/application/markdown-frontmatter.js +0 -2
  103. package/src/client/application/mermaid-preview-hydrator.js +1 -14
  104. package/src/client/application/pdf-preview-controller.js +992 -0
  105. package/src/client/application/plantuml-preview-hydrator.js +0 -10
  106. package/src/client/application/preview-diagram-utils.js +31 -2
  107. package/src/client/application/preview-render-compiler.js +2 -1
  108. package/src/client/application/preview-renderer.js +2 -2
  109. package/src/client/application/quick-switcher-loader.js +3 -1
  110. package/src/client/application/structurizr-preview-controller.js +395 -0
  111. package/src/client/application/workspace-coordinator.js +25 -3
  112. package/src/client/application/workspace-preview-controller.js +91 -5
  113. package/src/client/bootstrap/collabmd-app-shell.js +155 -43
  114. package/src/client/browser-utils.js +44 -0
  115. package/src/client/domain/excalidraw-collaboration.js +0 -2
  116. package/src/client/domain/room.js +38 -0
  117. package/src/client/domain/runtime-paths.js +26 -5
  118. package/src/client/domain/structurizr-language.js +99 -0
  119. package/src/client/domain/vault-paths.js +48 -0
  120. package/src/client/excalidraw-editor.js +833 -51
  121. package/src/client/export/export-pipeline.js +6 -34
  122. package/src/client/infrastructure/auth-client.js +4 -10
  123. package/src/client/infrastructure/browser-notification-port.js +33 -20
  124. package/src/client/infrastructure/browser-preferences-port.js +46 -9
  125. package/src/client/infrastructure/comment-thread-store.js +34 -13
  126. package/src/client/infrastructure/editor-session.js +10 -0
  127. package/src/client/infrastructure/editor-view-adapter.js +133 -5
  128. package/src/client/infrastructure/excalidraw-room-client.js +103 -14
  129. package/src/client/infrastructure/git-api-client.js +5 -0
  130. package/src/client/infrastructure/lobby-presence.js +1 -0
  131. package/src/client/infrastructure/vault-api-client.js +51 -0
  132. package/src/client/infrastructure/workspace-sync-client.js +13 -1
  133. package/src/client/presentation/comment-overview-controller.js +18 -53
  134. package/src/client/presentation/comment-ui/comment-ui-card.js +1 -12
  135. package/src/client/presentation/comment-ui/comment-ui-layout.js +242 -219
  136. package/src/client/presentation/comment-ui/comment-ui-render.js +32 -45
  137. package/src/client/presentation/comment-ui/comment-ui-shared.js +68 -13
  138. package/src/client/presentation/comment-ui-controller.js +5 -1
  139. package/src/client/presentation/drawio-embed-controller.js +9 -102
  140. package/src/client/presentation/drawio-viewer.js +118 -0
  141. package/src/client/presentation/excalidraw-embed-controller.js +74 -8
  142. package/src/client/presentation/file-action-controller.js +134 -0
  143. package/src/client/presentation/file-explorer-controller.js +33 -3
  144. package/src/client/presentation/file-explorer-view.js +59 -34
  145. package/src/client/presentation/file-icon-svg.js +40 -0
  146. package/src/client/presentation/file-tree-state.js +12 -4
  147. package/src/client/presentation/git-diff-view-controller.js +731 -3
  148. package/src/client/presentation/outline-controller.js +86 -0
  149. package/src/client/presentation/quick-switcher-controller.js +257 -74
  150. package/src/client/presentation/quick-switcher-text-search.js +3 -3
  151. package/src/client/presentation/toast-controller.js +3 -0
  152. package/src/client/styles/components/feedback.css +6 -0
  153. package/src/client/styles/components/navigation.css +11 -1
  154. package/src/client/styles/features/collaboration-chat.css +49 -53
  155. package/src/client/styles/features/collaboration-presence.css +9 -7
  156. package/src/client/styles/features/comment-card.css +25 -11
  157. package/src/client/styles/features/comment-overlays.css +0 -4
  158. package/src/client/styles/features/comment-overview.css +44 -21
  159. package/src/client/styles/features/comments-drawer.css +2 -100
  160. package/src/client/styles/features/diagram-preview.css +81 -18
  161. package/src/client/styles/features/diff-special.css +309 -0
  162. package/src/client/styles/features/embedded-preview.css +8 -33
  163. package/src/client/styles/features/quick-switcher.css +8 -0
  164. package/src/client/styles/foundation/themes.css +6 -0
  165. package/src/client/styles/foundation/tokens.css +1 -0
  166. package/src/client/styles/layout/editor-page.css +40 -0
  167. package/src/client/styles/layout/responsive.css +31 -1
  168. package/src/client/styles/layout/shell.css +383 -7
  169. package/src/client/styles/layout/sidebar.css +47 -15
  170. package/src/client/styles/layout/view-modes.css +1 -0
  171. package/src/client/styles/overrides/codemirror.css +217 -15
  172. package/src/client/styles/style.css +1 -0
  173. package/src/client/styles/surfaces/excalidraw-editor.css +294 -0
  174. package/src/domain/comment-threads.js +63 -5
  175. package/src/domain/excalidraw-room-codec.js +6 -4
  176. package/src/domain/file-kind.js +35 -3
  177. package/src/server/application/workspace-reconciliation.js +1 -1
  178. package/src/server/config/env.js +79 -0
  179. package/src/server/create-app-server.js +14 -3
  180. package/src/server/domain/bases/base-definition.js +1 -1
  181. package/src/server/domain/bases/base-expression-runtime.js +1 -4
  182. package/src/server/domain/bases/base-index-snapshot-store.js +1 -4
  183. package/src/server/domain/bases/base-query-service.js +1 -77
  184. package/src/server/domain/bases/base-transform.js +1 -4
  185. package/src/server/domain/collaboration/collaboration-room.js +55 -21
  186. package/src/server/domain/comment-overview.js +25 -8
  187. package/src/server/domain/docx-exporter.js +24 -33
  188. package/src/server/domain/ripgrep-search-service.js +7 -1
  189. package/src/server/domain/workspace-state.js +32 -11
  190. package/src/server/infrastructure/git/diff-service.js +29 -13
  191. package/src/server/infrastructure/git/git-service.js +61 -10
  192. package/src/server/infrastructure/git/history-service.js +96 -29
  193. package/src/server/infrastructure/git/image-mime.js +14 -0
  194. package/src/server/infrastructure/git/path-utils.js +1 -1
  195. package/src/server/infrastructure/git/responses.js +0 -95
  196. package/src/server/infrastructure/git/untracked-files.js +28 -1
  197. package/src/server/infrastructure/github/github-app-client.js +22 -15
  198. package/src/server/infrastructure/http/create-git-api-query-handler.js +31 -1
  199. package/src/server/infrastructure/http/create-request-handler.js +18 -31
  200. package/src/server/infrastructure/http/create-static-handler.js +5 -2
  201. package/src/server/infrastructure/http/create-structurizr-api-handler.js +145 -0
  202. package/src/server/infrastructure/http/create-vault-api-command-handler.js +70 -8
  203. package/src/server/infrastructure/http/create-vault-api-query-handler.js +51 -3
  204. package/src/server/infrastructure/http/http-errors.js +7 -1
  205. package/src/server/infrastructure/http/http-request-helpers.js +22 -0
  206. package/src/server/infrastructure/persistence/path-utils.js +1 -1
  207. package/src/server/infrastructure/persistence/vault-file-store.js +3 -0
  208. package/src/server/infrastructure/structurizr/structurizr-embed.css +43 -0
  209. package/src/server/infrastructure/structurizr/structurizr-workspace-service.js +374 -0
  210. package/src/server/infrastructure/websocket/attach-collaboration-gateway.js +4 -22
  211. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CbSISC1w.js +0 -1
  212. package/dist/client/assets/bases-preview-controller-2pLHk0uk.js +0 -143
  213. package/dist/client/assets/browser-utils-CKtI_rZl.js +0 -1
  214. package/dist/client/assets/button-BCEiw4Rw.js +0 -1
  215. package/dist/client/assets/chunk-AXR3VHVW-0GTk_evH.js +0 -41
  216. package/dist/client/assets/comment-threads-D-iKphIp.js +0 -4
  217. package/dist/client/assets/diagram-action-icons-aa_KOv9h.js +0 -36
  218. package/dist/client/assets/dist-BH87JoHx.js +0 -11
  219. package/dist/client/assets/dist-BIkPRZO7.js +0 -6
  220. package/dist/client/assets/dist-BqCsFpx4.js +0 -1
  221. package/dist/client/assets/dist-C2JHCt8U.js +0 -1
  222. package/dist/client/assets/dist-CZjTtmTu.js +0 -1
  223. package/dist/client/assets/dist-Cig4Ov_X.js +0 -1
  224. package/dist/client/assets/dist-CqQEC8gR.js +0 -1
  225. package/dist/client/assets/dist-D4Rzqod1.js +0 -1
  226. package/dist/client/assets/dist-D5sn-mwk.js +0 -9
  227. package/dist/client/assets/dist-D7G-n87v.js +0 -1
  228. package/dist/client/assets/dist-DRqOrNgN.js +0 -1
  229. package/dist/client/assets/dist-DetkMS_4.js +0 -1
  230. package/dist/client/assets/dist-Dp6FVkQ1.js +0 -1
  231. package/dist/client/assets/dist-DtbjNB28.js +0 -1
  232. package/dist/client/assets/dist-DyrYcvpo2.js +0 -1
  233. package/dist/client/assets/dist-GrygD2zE.js +0 -1
  234. package/dist/client/assets/dist-HmEpWUow.js +0 -1
  235. package/dist/client/assets/dist-RTa81Km7.js +0 -1
  236. package/dist/client/assets/drawio-editor-DDvFoarP.js +0 -9
  237. package/dist/client/assets/drawio-embed-controller-BD1GTRwn.js +0 -1
  238. package/dist/client/assets/drawioEditor-Bu5grlH5.js +0 -2
  239. package/dist/client/assets/editor-session-DGlpiAsK.js +0 -16
  240. package/dist/client/assets/embedded-editor-base-C9roKYpW.css +0 -1
  241. package/dist/client/assets/excalidraw-editor-BHu6k8d4.js +0 -9
  242. package/dist/client/assets/excalidraw-editor-Cr_184EF.css +0 -1
  243. package/dist/client/assets/excalidraw-embed-controller-y2fenN_g.js +0 -1
  244. package/dist/client/assets/excalidraw-embed-reconciler-DGiagExg.js +0 -1
  245. package/dist/client/assets/excalidraw-scene-BvsQ6mLP.js +0 -176
  246. package/dist/client/assets/excalidrawEditor-4vFE--Uw.js +0 -2
  247. package/dist/client/assets/export-page-Cby8JzPO.js +0 -39
  248. package/dist/client/assets/exportDocument-C9v-bc7I.css +0 -1
  249. package/dist/client/assets/exportDocument-CRHCfsdo.js +0 -2
  250. package/dist/client/assets/file-history-view-controller-BDjEV9xJ.js +0 -112
  251. package/dist/client/assets/file-kind-BKPOIRPE.js +0 -1
  252. package/dist/client/assets/git-diff-view-controller-DgdM1aqb.js +0 -150
  253. package/dist/client/assets/git-panel-controller-BDTR2HVp.js +0 -207
  254. package/dist/client/assets/image-XGD7JQR6-XDUWHRpO.js +0 -1
  255. package/dist/client/assets/index-7xOyXDhP.css +0 -1
  256. package/dist/client/assets/index-Bz1KuQ6y.js +0 -2
  257. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  258. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  259. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  260. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  261. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  262. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  263. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  264. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  265. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  266. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  267. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  268. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  269. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  270. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  271. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  272. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  273. package/dist/client/assets/main-Ci07m9oP.js +0 -152
  274. package/dist/client/assets/preview-diagram-utils-n_bbJA9V.js +0 -1
  275. package/dist/client/assets/preview-render-compiler-csGM5ZFz.js +0 -1
  276. package/dist/client/assets/quick-switcher-controller-CoiYvllm.js +0 -12
  277. package/dist/client/assets/room-CDrmIdDt.js +0 -1
  278. package/dist/client/assets/runtime-config-CDgeuPWN.js +0 -1
  279. package/dist/client/assets/runtime-config-loader-BHGniHP5.js +0 -1
  280. package/dist/client/assets/runtime-paths-DvpihCEq.js +0 -1
  281. package/dist/client/assets/segmented-control-DJrHSz8M.js +0 -1
  282. package/dist/client/assets/vault-api-client-sLB2bo56.js +0 -1
  283. package/dist/client/assets/vault-paths-CVkuh0j-.js +0 -5
  284. package/src/client/bootstrap/lazy-controller-feature.js +0 -433
  285. package/src/client/infrastructure/backlinks-api-client.js +0 -16
  286. package/src/client/infrastructure/plantuml-api-client.js +0 -19
  287. package/src/client/infrastructure/runtime-config-loader.js +0 -56
  288. package/src/server/domain/collaboration/room-persistence-controller.js +0 -80
  289. package/src/server/infrastructure/git/command-runner.js +0 -49
  290. package/src/server/infrastructure/git/errors.js +0 -8
  291. package/src/server/infrastructure/http/create-esm-proxy-handler.js +0 -130
@@ -4,20 +4,25 @@ const EXCALIDRAW_FILE_EXTENSION = '.excalidraw';
4
4
  const DRAWIO_FILE_EXTENSION = '.drawio';
5
5
  const MERMAID_FILE_EXTENSIONS = Object.freeze(['.mmd', '.mermaid']);
6
6
  const PLANTUML_FILE_EXTENSIONS = Object.freeze(['.puml', '.plantuml']);
7
+ const STRUCTURIZR_FILE_EXTENSION = '.dsl';
8
+ const STRUCTURIZR_FILE_EXTENSIONS = Object.freeze([STRUCTURIZR_FILE_EXTENSION]);
9
+ const PDF_FILE_EXTENSION = '.pdf';
7
10
  const IMAGE_ATTACHMENT_EXTENSIONS = Object.freeze(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg']);
8
11
  const DIAGRAM_FILE_EXTENSIONS = Object.freeze([
9
12
  EXCALIDRAW_FILE_EXTENSION,
10
13
  DRAWIO_FILE_EXTENSION,
11
14
  ...MERMAID_FILE_EXTENSIONS,
12
15
  ...PLANTUML_FILE_EXTENSIONS,
16
+ ...STRUCTURIZR_FILE_EXTENSIONS,
13
17
  ]);
14
18
  const VAULT_FILE_EXTENSIONS = Object.freeze([
15
19
  ...MARKDOWN_FILE_EXTENSIONS,
16
20
  BASE_FILE_EXTENSION,
17
21
  ...DIAGRAM_FILE_EXTENSIONS,
22
+ PDF_FILE_EXTENSION,
18
23
  ...IMAGE_ATTACHMENT_EXTENSIONS,
19
24
  ]);
20
- const STRIP_VAULT_EXTENSION_PATTERN = /\.(?:md|markdown|mdx|base|excalidraw|drawio|mmd|mermaid|puml|plantuml|png|jpe?g|webp|gif|svg)$/i;
25
+ const STRIP_VAULT_EXTENSION_PATTERN = /\.(?:md|markdown|mdx|base|excalidraw|drawio|mmd|mermaid|puml|plantuml|dsl|pdf|png|jpe?g|webp|gif|svg)$/i;
21
26
 
22
27
  function normalizeFilePath(filePath) {
23
28
  return String(filePath ?? '').trim().toLowerCase();
@@ -36,7 +41,10 @@ export {
36
41
  IMAGE_ATTACHMENT_EXTENSIONS,
37
42
  MARKDOWN_FILE_EXTENSIONS,
38
43
  MERMAID_FILE_EXTENSIONS,
44
+ PDF_FILE_EXTENSION,
39
45
  PLANTUML_FILE_EXTENSIONS,
46
+ STRUCTURIZR_FILE_EXTENSION,
47
+ STRUCTURIZR_FILE_EXTENSIONS,
40
48
  VAULT_FILE_EXTENSIONS,
41
49
  };
42
50
 
@@ -65,6 +73,14 @@ export function getVaultFileKind(filePath) {
65
73
  return 'plantuml';
66
74
  }
67
75
 
76
+ if (hasFileExtension(filePath, STRUCTURIZR_FILE_EXTENSIONS)) {
77
+ return 'structurizr';
78
+ }
79
+
80
+ if (hasFileExtension(filePath, [PDF_FILE_EXTENSION])) {
81
+ return 'pdf';
82
+ }
83
+
68
84
  if (hasFileExtension(filePath, IMAGE_ATTACHMENT_EXTENSIONS)) {
69
85
  return 'image';
70
86
  }
@@ -118,13 +134,25 @@ export function isPlantUmlFilePath(filePath) {
118
134
  return getVaultFileKind(filePath) === 'plantuml';
119
135
  }
120
136
 
137
+ export function isStructurizrFilePath(filePath) {
138
+ return getVaultFileKind(filePath) === 'structurizr';
139
+ }
140
+
141
+ export function isPdfFilePath(filePath) {
142
+ return getVaultFileKind(filePath) === 'pdf';
143
+ }
144
+
121
145
  export function isImageAttachmentFilePath(filePath) {
122
146
  return getVaultFileKind(filePath) === 'image';
123
147
  }
124
148
 
125
149
  export function isDiagramFilePath(filePath) {
126
150
  const kind = getVaultFileKind(filePath);
127
- return kind === 'excalidraw' || kind === 'drawio' || kind === 'mermaid' || kind === 'plantuml';
151
+ return kind === 'excalidraw'
152
+ || kind === 'drawio'
153
+ || kind === 'mermaid'
154
+ || kind === 'plantuml'
155
+ || kind === 'structurizr';
128
156
  }
129
157
 
130
158
  export function isVaultFilePath(filePath) {
@@ -133,7 +161,11 @@ export function isVaultFilePath(filePath) {
133
161
 
134
162
  export function supportsCommentsForFilePath(filePath) {
135
163
  const kind = getVaultFileKind(filePath);
136
- return kind === 'markdown' || kind === 'mermaid' || kind === 'plantuml';
164
+ return kind === 'markdown'
165
+ || kind === 'mermaid'
166
+ || kind === 'plantuml'
167
+ || kind === 'structurizr'
168
+ || kind === 'excalidraw';
137
169
  }
138
170
 
139
171
  export function supportsBacklinksForFilePath(filePath) {
@@ -135,7 +135,7 @@ export class WorkspaceReconciliation {
135
135
  async initialize({ snapshot = null } = {}) {
136
136
  const effectiveSnapshot = snapshot ?? await this.vaultFileStore.scanWorkspaceState();
137
137
  this.replaceWorkspaceState(effectiveSnapshot);
138
- await this.baseQueryService?.initializeFromWorkspaceState?.(effectiveSnapshot);
138
+ await this.baseQueryService?.snapshotStore?.initializeFromWorkspaceState?.(effectiveSnapshot);
139
139
  this.getWorkspaceRoom()?.replaceWorkspaceEntries(effectiveSnapshot.entries, {
140
140
  generatedAt: effectiveSnapshot.scannedAt,
141
141
  });
@@ -13,11 +13,28 @@ import {
13
13
  import { loadBuildInfo } from './build-info.js';
14
14
  import { isPerfLoggingEnabled } from './perf-logging.js';
15
15
 
16
+ const DEFAULT_SEARCH_MAX_BUFFER_BYTES = 2 * 1024 * 1024;
17
+ const DEFAULT_SEARCH_MAX_FILE_SIZE = '1M';
18
+ const DEFAULT_PDF_UPLOAD_BYTES = 50 * 1024 * 1024;
19
+
16
20
  function parsePositiveInt(rawValue, fallbackValue) {
17
21
  const parsed = Number.parseInt(rawValue ?? '', 10);
18
22
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallbackValue;
19
23
  }
20
24
 
25
+ function normalizeSearchMaxFileSize(rawValue, fallbackValue = DEFAULT_SEARCH_MAX_FILE_SIZE) {
26
+ const normalized = String(rawValue ?? '').trim();
27
+ if (!normalized) {
28
+ return fallbackValue;
29
+ }
30
+
31
+ if (!/^[1-9]\d*(?:[KMG])?$/iu.test(normalized)) {
32
+ throw new Error('COLLABMD_SEARCH_MAX_FILE_SIZE must be a positive byte count with an optional K, M, or G suffix.');
33
+ }
34
+
35
+ return normalized.toUpperCase();
36
+ }
37
+
21
38
  function parseOptionalPositiveInt(rawValue, {
22
39
  fallbackValue = null,
23
40
  variableName = 'value',
@@ -56,6 +73,30 @@ function resolveDrawioBaseUrl(value) {
56
73
  return normalizeOptionalString(value) || 'https://embed.diagrams.net';
57
74
  }
58
75
 
76
+ function resolveStructurizrServerUrl(value) {
77
+ const normalized = normalizeOptionalString(value);
78
+ if (!normalized) {
79
+ return '';
80
+ }
81
+
82
+ let parsedUrl;
83
+ try {
84
+ parsedUrl = new URL(normalized);
85
+ } catch {
86
+ throw new Error('STRUCTURIZR_SERVER_URL must be an absolute HTTP or HTTPS URL.');
87
+ }
88
+
89
+ if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
90
+ throw new Error('STRUCTURIZR_SERVER_URL must use HTTP or HTTPS.');
91
+ }
92
+
93
+ if (parsedUrl.search || parsedUrl.hash) {
94
+ throw new Error('STRUCTURIZR_SERVER_URL must not include a query string or hash.');
95
+ }
96
+
97
+ return normalized.replace(/\/+$/u, '');
98
+ }
99
+
59
100
  function normalizeAppBasePath(basePath) {
60
101
  const normalized = normalizeOptionalString(basePath);
61
102
  if (!normalized || normalized === '/') {
@@ -278,6 +319,31 @@ function loadOidcConfig(overrides = {}, { basePath = '' } = {}) {
278
319
  };
279
320
  }
280
321
 
322
+ function loadStructurizrConfig(overrides = {}, { vaultDir } = {}) {
323
+ const serverUrl = resolveStructurizrServerUrl(
324
+ overrides.serverUrl
325
+ ?? process.env.COLLABMD_STRUCTURIZR_SERVER_URL
326
+ ?? process.env.STRUCTURIZR_SERVER_URL,
327
+ );
328
+ const mirrorDir = resolveOptionalPath(
329
+ overrides.mirrorDir
330
+ ?? process.env.COLLABMD_STRUCTURIZR_MIRROR_DIR
331
+ ?? process.env.STRUCTURIZR_MIRROR_DIR,
332
+ ) || resolve(vaultDir, '.collabmd/structurizr');
333
+
334
+ return {
335
+ enabled: Boolean(serverUrl),
336
+ mirrorDir,
337
+ serverUrl,
338
+ trustedExecutableDsl: overrides.trustedExecutableDsl
339
+ ?? parseBooleanFlag(
340
+ process.env.COLLABMD_STRUCTURIZR_TRUSTED_EXECUTABLE_DSL
341
+ ?? process.env.STRUCTURIZR_TRUSTED_EXECUTABLE_DSL,
342
+ false,
343
+ ),
344
+ };
345
+ }
346
+
281
347
  function loadHostedConfig(overrides = {}, { authStrategy, vaultDir } = {}) {
282
348
  const enabled = overrides.enabled ?? parseBooleanFlag(process.env.COLLABMD_HOSTED_ENABLED, false);
283
349
  const metadataDbPath = resolveOptionalPath(
@@ -361,6 +427,7 @@ export function loadConfig(overrides = {}) {
361
427
  : null;
362
428
  const git = loadGitConfig(overrides.git);
363
429
  const hosted = loadHostedConfig(overrides.hosted, { authStrategy, vaultDir });
430
+ const structurizr = loadStructurizrConfig(overrides.structurizr, { vaultDir });
364
431
  const build = loadBuildInfo({
365
432
  explicitBuildId: process.env.COLLABMD_BUILD_ID,
366
433
  projectRoot,
@@ -396,6 +463,18 @@ export function loadConfig(overrides = {}) {
396
463
  maxBaseQueryRows: parsePositiveInt(process.env.COLLABMD_MAX_BASE_QUERY_ROWS, 5_000),
397
464
  maxDownloadFileBytes: parsePositiveInt(process.env.COLLABMD_MAX_DOWNLOAD_FILE_BYTES, 268_435_456),
398
465
  maxInitialSyncBytes: parsePositiveInt(process.env.COLLABMD_MAX_INITIAL_SYNC_BYTES, 16_777_216),
466
+ maxPdfUploadBytes: parsePositiveInt(
467
+ overrides.maxPdfUploadBytes ?? process.env.COLLABMD_MAX_PDF_UPLOAD_BYTES,
468
+ DEFAULT_PDF_UPLOAD_BYTES,
469
+ ),
470
+ searchMaxBufferBytes: parsePositiveInt(
471
+ overrides.searchMaxBufferBytes ?? process.env.COLLABMD_SEARCH_MAX_BUFFER_BYTES,
472
+ DEFAULT_SEARCH_MAX_BUFFER_BYTES,
473
+ ),
474
+ searchMaxFileSize: normalizeSearchMaxFileSize(
475
+ overrides.searchMaxFileSize ?? process.env.COLLABMD_SEARCH_MAX_FILE_SIZE,
476
+ ),
477
+ structurizr,
399
478
  perfLoggingEnabled: overrides.perfLoggingEnabled ?? isPerfLoggingEnabled(process.env.COLLABMD_PERF_LOGGING),
400
479
  port: parsePositiveInt(process.env.PORT, 1234),
401
480
  nodeEnv,
@@ -1,4 +1,5 @@
1
1
  import { createServer } from 'http';
2
+ import { resolve } from 'node:path';
2
3
 
3
4
  import { loadConfig } from './config/env.js';
4
5
  import { createAuthService } from './auth/create-auth-service.js';
@@ -7,12 +8,13 @@ import { BacklinkIndex } from './domain/backlink-index.js';
7
8
  import { BaseQueryService } from './domain/bases/base-query-service.js';
8
9
  import { CollaborationDocumentStore } from './domain/collaboration/collaboration-document-store.js';
9
10
  import { CollaborationRoom } from './domain/collaboration/collaboration-room.js';
10
- import { DocxExporter } from './domain/docx-exporter.js';
11
+ import { renderDocx } from './domain/docx-exporter.js';
11
12
  import { GitService } from './infrastructure/git/git-service.js';
12
13
  import { GitHubAppClient } from './infrastructure/github/github-app-client.js';
13
14
  import { GitHubSetupFlow } from './infrastructure/github/github-setup-flow.js';
14
15
  import { HostedWorkspaceService } from './domain/hosted-workspace.js';
15
16
  import { PlantUmlRenderer } from './infrastructure/plantuml/plantuml-renderer.js';
17
+ import { StructurizrWorkspaceService } from './infrastructure/structurizr/structurizr-workspace-service.js';
16
18
  import { RoomRegistry } from './domain/collaboration/room-registry.js';
17
19
  import { RipgrepSearchService } from './domain/ripgrep-search-service.js';
18
20
  import { createRequestHandler } from './infrastructure/http/create-request-handler.js';
@@ -80,16 +82,23 @@ export function createAppServer(config = loadConfig()) {
80
82
  workspaceStateProvider: () => workspaceMutationCoordinator?.workspaceState ?? null,
81
83
  workspaceStateSynchronizer: () => fileSystemSyncService?.flushPendingChanges?.(),
82
84
  });
83
- const docxExporter = new DocxExporter();
84
85
  const plantUmlRenderer = new PlantUmlRenderer({
85
86
  serverUrl: config.plantumlServerUrl,
86
87
  });
88
+ const structurizrWorkspaceService = new StructurizrWorkspaceService({
89
+ mirrorDir: config.structurizr?.mirrorDir || resolve(config.vaultDir, '.collabmd/structurizr'),
90
+ serverUrl: config.structurizr?.serverUrl || '',
91
+ trustedExecutableDsl: config.structurizr?.trustedExecutableDsl,
92
+ vaultDir: config.vaultDir,
93
+ });
87
94
  const gitService = new GitService({
88
95
  commandEnv: config.git?.commandEnv,
89
96
  enabled: config.gitEnabled,
90
97
  vaultDir: config.vaultDir,
91
98
  });
92
99
  const searchService = new RipgrepSearchService({
100
+ maxBufferBytes: config.searchMaxBufferBytes,
101
+ maxFileSize: config.searchMaxFileSize,
93
102
  perfLoggingEnabled: config.perfLoggingEnabled,
94
103
  vaultDir: config.vaultDir,
95
104
  });
@@ -144,7 +153,7 @@ export function createAppServer(config = loadConfig()) {
144
153
  vaultFileStore,
145
154
  backlinkIndex,
146
155
  baseQueryService,
147
- docxExporter,
156
+ renderDocx,
148
157
  roomRegistry,
149
158
  plantUmlRenderer,
150
159
  gitService,
@@ -154,6 +163,7 @@ export function createAppServer(config = loadConfig()) {
154
163
  fileSystemSyncService,
155
164
  hostedWorkspaceService,
156
165
  githubSetupFlow,
166
+ structurizrWorkspaceService,
157
167
  );
158
168
  const httpServer = createServer((req, res) => {
159
169
  requestHandler(req, res).catch((error) => {
@@ -316,6 +326,7 @@ export function createAppServer(config = loadConfig()) {
316
326
  gitService,
317
327
  hostedWorkspaceService,
318
328
  searchService,
329
+ structurizrWorkspaceService,
319
330
  setTestHydrateDelayMs(delayMs = 0) {
320
331
  testControls.wsRoomHydrateDelayMs = Math.max(0, Number(delayMs) || 0);
321
332
  },
@@ -2,7 +2,7 @@ import { dump as dumpYaml, loadAll as loadAllYaml } from 'js-yaml';
2
2
 
3
3
  const SUPPORTED_VIEW_TYPES = new Set(['cards', 'list', 'table']);
4
4
 
5
- function isPlainObject(value) {
5
+ export function isPlainObject(value) {
6
6
  return Object.prototype.toString.call(value) === '[object Object]';
7
7
  }
8
8
 
@@ -1,9 +1,6 @@
1
1
  import { isImageAttachmentFilePath } from '../../../domain/file-kind.js';
2
2
  import { resolveVaultRelativePath } from '../../../domain/vault-paths.js';
3
-
4
- function isPlainObject(value) {
5
- return Object.prototype.toString.call(value) === '[object Object]';
6
- }
3
+ import { isPlainObject } from './base-definition.js';
7
4
 
8
5
  export function createLinkValue(target, {
9
6
  display = '',
@@ -12,6 +12,7 @@ import {
12
12
  createReferenceTargetAliasMap,
13
13
  } from '../markdown-reference-extractor.js';
14
14
  import { mapWithConcurrency } from '../../shared/async-utils.js';
15
+ import { isPlainObject } from './base-definition.js';
15
16
  import { createLinkValue, dedupeLinkValues } from './base-expression-runtime.js';
16
17
 
17
18
  const INLINE_TAG_RE = /(^|[\s(])#([A-Za-z0-9/_-]+)/g;
@@ -39,10 +40,6 @@ function createRowCacheKey(filePath, workspaceState, fileSetSignature) {
39
40
  ].join('\0');
40
41
  }
41
42
 
42
- function isPlainObject(value) {
43
- return Object.prototype.toString.call(value) === '[object Object]';
44
- }
45
-
46
43
  function normalizeTags(value) {
47
44
  if (Array.isArray(value)) {
48
45
  return value
@@ -152,82 +152,6 @@ export class BaseQueryService {
152
152
  this.propertyCatalogCache = new Map();
153
153
  }
154
154
 
155
- get workspaceStateProvider() {
156
- return this.snapshotStore.workspaceStateProvider;
157
- }
158
-
159
- set workspaceStateProvider(value) {
160
- this.snapshotStore.workspaceStateProvider = value;
161
- }
162
-
163
- get workspaceStateSynchronizer() {
164
- return this.snapshotStore.workspaceStateSynchronizer;
165
- }
166
-
167
- set workspaceStateSynchronizer(value) {
168
- this.snapshotStore.workspaceStateSynchronizer = value;
169
- }
170
-
171
- get indexSnapshot() {
172
- return this.snapshotStore.indexSnapshot;
173
- }
174
-
175
- set indexSnapshot(value) {
176
- this.snapshotStore.indexSnapshot = value;
177
- }
178
-
179
- get lastWorkspaceState() {
180
- return this.snapshotStore.lastWorkspaceState;
181
- }
182
-
183
- set lastWorkspaceState(value) {
184
- this.snapshotStore.lastWorkspaceState = value;
185
- }
186
-
187
- async getWorkspaceState() {
188
- return this.snapshotStore.getWorkspaceState();
189
- }
190
-
191
- createSnapshotRow(...args) {
192
- return this.snapshotStore.createSnapshotRow(...args);
193
- }
194
-
195
- rebuildBacklinks(...args) {
196
- return this.snapshotStore.rebuildBacklinks(...args);
197
- }
198
-
199
- async buildIndexSnapshot(...args) {
200
- return this.snapshotStore.buildIndexSnapshot(...args);
201
- }
202
-
203
- async synchronizeWorkspaceState() {
204
- return this.snapshotStore.synchronizeWorkspaceState();
205
- }
206
-
207
- removeSnapshotPath(...args) {
208
- return this.snapshotStore.removeSnapshotPath(...args);
209
- }
210
-
211
- upsertSnapshotPath(...args) {
212
- return this.snapshotStore.upsertSnapshotPath(...args);
213
- }
214
-
215
- collectImpactedSourcesForMembershipChanges(...args) {
216
- return this.snapshotStore.collectImpactedSourcesForMembershipChanges(...args);
217
- }
218
-
219
- async refreshSnapshotRows(...args) {
220
- return this.snapshotStore.refreshSnapshotRows(...args);
221
- }
222
-
223
- async ensureIndexSnapshot(...args) {
224
- return this.snapshotStore.ensureIndexSnapshot(...args);
225
- }
226
-
227
- async initializeFromWorkspaceState(...args) {
228
- return this.snapshotStore.initializeFromWorkspaceState(...args);
229
- }
230
-
231
155
  async applyWorkspaceChange(...args) {
232
156
  const result = await this.snapshotStore.applyWorkspaceChange(...args);
233
157
  const scannedAt = this.snapshotStore.indexSnapshot?.scannedAt;
@@ -279,7 +203,7 @@ export class BaseQueryService {
279
203
  }
280
204
 
281
205
  const definition = this.getCachedDefinition(source === null ? `file:${basePath}` : `source:${sourcePath}:${basePath}`, baseSource);
282
- const snapshot = await this.ensureIndexSnapshot({
206
+ const snapshot = await this.snapshotStore.ensureIndexSnapshot({
283
207
  basePath,
284
208
  sourcePath,
285
209
  });
@@ -2,15 +2,12 @@ import { dump as dumpYaml } from 'js-yaml';
2
2
 
3
3
  import {
4
4
  findView,
5
+ isPlainObject,
5
6
  normalizeBaseDefinition,
6
7
  normalizeRawDefinitionForWrite,
7
8
  parseBaseSource,
8
9
  } from './base-definition.js';
9
10
 
10
- function isPlainObject(value) {
11
- return Object.prototype.toString.call(value) === '[object Object]';
12
- }
13
-
14
11
  function cloneRawSource(source = '') {
15
12
  const parsed = parseBaseSource(source);
16
13
  return isPlainObject(parsed) ? parsed : {};
@@ -6,9 +6,9 @@ import * as decoding from 'lib0/decoding';
6
6
 
7
7
  import { MSG_AWARENESS, MSG_SYNC } from './protocol.js';
8
8
  import { CollaborationDocumentStore } from './collaboration-document-store.js';
9
- import { RoomPersistenceController } from './room-persistence-controller.js';
10
9
  import { logPerfEvent } from '../../config/perf-logging.js';
11
10
  import { populateCommentThreads, serializeCommentThreads } from '../../../domain/comment-threads.js';
11
+ import { getVaultFileKind } from '../../../domain/file-kind.js';
12
12
  import {
13
13
  EXCALIDRAW_APP_STATE_KEY,
14
14
  EXCALIDRAW_ELEMENTS_KEY,
@@ -123,7 +123,7 @@ function readAwarenessEntries(update) {
123
123
  }
124
124
 
125
125
  function isExcalidrawRoom(name) {
126
- return typeof name === 'string' && name.endsWith('.excalidraw');
126
+ return getVaultFileKind(name) === 'excalidraw';
127
127
  }
128
128
 
129
129
  function isWorkspaceRoom(name) {
@@ -214,15 +214,10 @@ export class CollaborationRoom {
214
214
  lastHydrate: null,
215
215
  lastInitialSyncAt: 0,
216
216
  };
217
- this.persistence = new RoomPersistenceController({
218
- idleGraceMs: this.idleGraceMs,
219
- onDestroy: () => {
220
- this.awareness.destroy();
221
- this.doc.destroy();
222
- this.onEmpty?.(this.name);
223
- },
224
- onPersist: () => this.persist(),
225
- });
217
+ this.persistTimer = null;
218
+ this.destroyTimer = null;
219
+ this.finalizePromise = null;
220
+ this.shutdownGeneration = 0;
226
221
 
227
222
  this.awareness.setLocalState(null);
228
223
  this.registerDocListeners();
@@ -475,11 +470,12 @@ export class CollaborationRoom {
475
470
  return;
476
471
  }
477
472
 
478
- this.persistence.schedulePersist(async () => {
473
+ clearTimeout(this.persistTimer);
474
+ this.persistTimer = setTimeout(() => {
479
475
  this.persist().catch((error) => {
480
476
  console.error(`[room:${this.name}] Failed to persist document:`, error.message);
481
477
  });
482
- });
478
+ }, 500);
483
479
  }
484
480
 
485
481
  async persist() {
@@ -620,9 +616,17 @@ export class CollaborationRoom {
620
616
  this.documentStore?.rename(nextName);
621
617
  }
622
618
 
619
+ cancelPersistenceTimers() {
620
+ this.shutdownGeneration += 1;
621
+ clearTimeout(this.persistTimer);
622
+ clearTimeout(this.destroyTimer);
623
+ this.persistTimer = null;
624
+ this.destroyTimer = null;
625
+ }
626
+
623
627
  markDeleted() {
624
628
  this.deleted = true;
625
- this.persistence.cancelAll();
629
+ this.cancelPersistenceTimers();
626
630
  if (this.clients.size === 0) {
627
631
  this.finalizeIfIdle();
628
632
  }
@@ -669,7 +673,9 @@ export class CollaborationRoom {
669
673
  }
670
674
 
671
675
  async addClient(ws, { sendInitialSync: shouldSendInitialSync = true } = {}) {
672
- this.persistence.markActivity();
676
+ this.shutdownGeneration += 1;
677
+ clearTimeout(this.destroyTimer);
678
+ this.destroyTimer = null;
673
679
  await this.hydrate();
674
680
 
675
681
  this.ensureClientState(ws);
@@ -721,9 +727,9 @@ export class CollaborationRoom {
721
727
  }
722
728
 
723
729
  this.destroyed = true;
724
- this.persistence.cancelAll();
730
+ this.cancelPersistenceTimers();
725
731
  await Promise.allSettled([
726
- this.persistence.finalizePromise,
732
+ this.finalizePromise,
727
733
  this.activePersistPromise,
728
734
  ]);
729
735
 
@@ -745,12 +751,40 @@ export class CollaborationRoom {
745
751
  }
746
752
 
747
753
  finalizeIfIdle() {
748
- return this.persistence.finalizeIfIdle({
749
- isIdle: () => this.clients.size === 0,
750
- onPersistError: (error) => {
754
+ if (this.clients.size > 0 || this.finalizePromise) {
755
+ return;
756
+ }
757
+
758
+ const generation = ++this.shutdownGeneration;
759
+ this.finalizePromise = (async () => {
760
+ clearTimeout(this.persistTimer);
761
+ this.persistTimer = null;
762
+
763
+ try {
764
+ await this.persist();
765
+ } catch (error) {
751
766
  console.error(`[room:${this.name}] Failed to persist final room state:`, error.message);
752
- },
767
+ }
768
+
769
+ if (this.clients.size > 0 || generation !== this.shutdownGeneration) {
770
+ return;
771
+ }
772
+
773
+ this.destroyTimer = setTimeout(() => {
774
+ if (this.clients.size > 0 || generation !== this.shutdownGeneration) {
775
+ return;
776
+ }
777
+
778
+ this.awareness.destroy();
779
+ this.doc.destroy();
780
+ this.onEmpty?.(this.name);
781
+ }, this.idleGraceMs);
782
+ this.destroyTimer.unref?.();
783
+ })().finally(() => {
784
+ this.finalizePromise = null;
753
785
  });
786
+
787
+ return this.finalizePromise;
754
788
  }
755
789
 
756
790
  clearEphemeralExcalidrawHistory() {
@@ -1,7 +1,14 @@
1
- import { summarizeCommentExcerpt } from '../../domain/comment-threads.js';
2
-
3
1
  const COMMENT_OVERVIEW_PREVIEW_MAX_LENGTH = 140;
4
2
 
3
+ function summarizeCommentMarkdown(value, maxLength = COMMENT_OVERVIEW_PREVIEW_MAX_LENGTH) {
4
+ const normalized = String(value ?? '').replace(/\r\n?/gu, '\n').trim();
5
+ if (normalized.length <= maxLength) {
6
+ return normalized;
7
+ }
8
+
9
+ return `${normalized.slice(0, Math.max(maxLength - 1, 1)).trimEnd()}…`;
10
+ }
11
+
5
12
  function asFiniteNumber(value) {
6
13
  return Number.isFinite(value) ? value : null;
7
14
  }
@@ -32,10 +39,16 @@ function createThreadSummary(thread) {
32
39
  }
33
40
 
34
41
  const id = asString(thread.id);
42
+ const anchorKind = asString(thread.anchorKind) || 'line';
43
+ const isDiagramElementAnchor = anchorKind === 'diagram-element';
35
44
  const anchorStartLine = asFiniteNumber(thread.anchorStartLine);
36
45
  const anchorEndLine = asFiniteNumber(thread.anchorEndLine);
37
46
  const messages = asArray(thread.messages);
38
- if (!id || anchorStartLine === null || anchorEndLine === null || messages.length === 0) {
47
+ if (
48
+ !id
49
+ || messages.length === 0
50
+ || (!isDiagramElementAnchor && (anchorStartLine === null || anchorEndLine === null))
51
+ ) {
39
52
  return null;
40
53
  }
41
54
 
@@ -46,10 +59,14 @@ function createThreadSummary(thread) {
46
59
 
47
60
  return {
48
61
  anchor: {
49
- endLine: Math.max(anchorEndLine, anchorStartLine),
50
- kind: asString(thread.anchorKind) || 'line',
51
- quote: asString(thread.anchorQuote),
52
- startLine: Math.max(anchorStartLine, 1),
62
+ ...(isDiagramElementAnchor
63
+ ? {}
64
+ : {
65
+ endLine: Math.max(anchorEndLine, anchorStartLine),
66
+ startLine: Math.max(anchorStartLine, 1),
67
+ }),
68
+ kind: anchorKind,
69
+ quote: asString(thread.anchorQuote) || (isDiagramElementAnchor ? 'Diagram element' : ''),
53
70
  },
54
71
  createdAt: asFiniteNumber(thread.createdAt) ?? latestActivityAt,
55
72
  createdByColor: asString(thread.createdByColor),
@@ -58,7 +75,7 @@ function createThreadSummary(thread) {
58
75
  latestActivityAt,
59
76
  latestMessage: latestMessage
60
77
  ? {
61
- bodyPreview: summarizeCommentExcerpt(latestMessage.body, COMMENT_OVERVIEW_PREVIEW_MAX_LENGTH),
78
+ bodyPreview: summarizeCommentMarkdown(latestMessage.body),
62
79
  createdAt: asFiniteNumber(latestMessage.createdAt) ?? latestActivityAt,
63
80
  userColor: asString(latestMessage.userColor),
64
81
  userName: asString(latestMessage.userName) || 'Anonymous',