collabmd 0.1.29 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/client/assets/{CodeMirrorEditor-UCMRAMCV-9k1Shd4H.js → CodeMirrorEditor-UCMRAMCV-tj0_ycIx.js} +1 -1
- package/dist/client/assets/architectureDiagram-2XIMDMQ5-BPG5b65m.js +36 -0
- package/dist/client/assets/base-CaIEJUF9.css +1 -0
- package/dist/client/assets/{blockDiagram-WCTKOSBZ-0rR9ivwb.js → blockDiagram-WCTKOSBZ-F38TRzI3.js} +1 -1
- package/dist/client/assets/{chunk-ETSVUAFD-CN-gPl_j.js → chunk-AXZTUZBK-Cxfm7hmm.js} +1 -1
- package/dist/client/assets/{chunk-GLR3WWYH-ZnuaBvXp.js → chunk-GLR3WWYH-CQ1LryrI.js} +2 -2
- package/dist/client/assets/{chunk-NQ4KR5QH-BCjGNd0g.js → chunk-NQ4KR5QH-BBX89uUa.js} +1 -1
- package/dist/client/assets/chunk-NU2UP6EN-CGeKI3BV.js +35 -0
- package/dist/client/assets/{chunk-WL4C6EOR-OuH0lN6M.js → chunk-WL4C6EOR-zSV-8h7F.js} +1 -1
- package/dist/client/assets/classDiagram-VBA2DB6C-BQqG4q1b.js +1 -0
- package/dist/client/assets/classDiagram-v2-RAHNMMFH-BUKbprX1.js +1 -0
- package/dist/client/assets/dagre-KLK3FWXG-CAG8WeES.js +4 -0
- package/dist/client/assets/diagram-E7M64L7V-Bm5FD0I5.js +24 -0
- package/dist/client/assets/diagram-IFDJBPK2-pqicH-av.js +43 -0
- package/dist/client/assets/diagram-P4PSJMXO-DwIXO27H.js +24 -0
- package/dist/client/assets/dist-3bQB_we3.js +1 -0
- package/dist/client/assets/dist-3uAMa5w5.js +1 -0
- package/dist/client/assets/{dist-D9c0A4Uh.js → dist-B-02vKOA.js} +1 -1
- package/dist/client/assets/{dist-A0eSMIyy.js → dist-BLAlZer_.js} +1 -1
- package/dist/client/assets/dist-BVB8rn7o.js +1 -0
- package/dist/client/assets/{dist-DveOCUUJ.js → dist-Bc2ND6i3.js} +1 -1
- package/dist/client/assets/{dist-BNcU46Ik.js → dist-BqQjnEWr.js} +1 -1
- package/dist/client/assets/{dist-Bay3vk5z.js → dist-BtnXjRx0.js} +1 -1
- package/dist/client/assets/{dist-CEYpWxod.js → dist-BzbYwa8m.js} +1 -1
- package/dist/client/assets/{dist-Ct1bmQnL.js → dist-BzpfHRaE.js} +1 -1
- package/dist/client/assets/{dist-BbKpzsTB.js → dist-CGV1S_as.js} +1 -1
- package/dist/client/assets/dist-CL-jjdvG.js +1 -0
- package/dist/client/assets/{dist-D0e6Qsad.js → dist-CXpKYW9s.js} +1 -1
- package/dist/client/assets/{dist-BchrxlnW.js → dist-CjII8VDS.js} +1 -1
- package/dist/client/assets/{dist-DnrO-JZd.js → dist-CxfohGIV.js} +1 -1
- package/dist/client/assets/{dist-CMHIjMVe.js → dist-D5QOYy7y.js} +1 -1
- package/dist/client/assets/{dist-CEtbxXlI.js → dist-DIi0uqLJ.js} +1 -1
- package/dist/client/assets/dist-DJ16N7ol.js +1 -0
- package/dist/client/assets/dist-DUvaQ-Bj.js +1 -0
- package/dist/client/assets/{dist-DnxPQ0-w.js → dist-DWJL3_bF.js} +6 -6
- package/dist/client/assets/dist-DsL7sqZR.js +1 -0
- package/dist/client/assets/{dist-DbhQ5ANS.js → dist-Dyt7n5Ui.js} +1 -1
- package/dist/client/assets/{dist-BOrlJDrR.js → dist-E_jPb93e.js} +1 -1
- package/dist/client/assets/{dist-D-gRgZ6e.js → dist-O1z0Ayg0.js} +1 -1
- package/dist/client/assets/dist-OQE68wIX.js +1 -0
- package/dist/client/assets/dist-k47-Io7D.js +1 -0
- package/dist/client/assets/{dist-Cdzvi8cf.js → dist-lDhnP-K1.js} +1 -1
- package/dist/client/assets/{dist-DS6mEQYy.js → dist-rMhng5DK.js} +1 -1
- package/dist/client/assets/dist-xO1ImhKz.js +1 -0
- package/dist/client/assets/{drawio-editor-CgbaOvpF.js → drawio-editor-CTZtMo1J.js} +1 -1
- package/dist/client/assets/{drawioEditor-Eb2yYhjE.js → drawioEditor-keSJVTcv.js} +2 -2
- package/dist/client/assets/editor-session-C9l9XwVg.js +16 -0
- package/dist/client/assets/{embedded-editor-base-DXRHYz6R.css → embedded-editor-base-DgmRcsR8.css} +1 -1
- package/dist/client/assets/{en-CTDJDZ6K-BHbXrqU0.js → en-IQZW2TQM-CZeIyvu2.js} +1 -1
- package/dist/client/assets/{erDiagram-INFDFZHY-DP3f20BF.js → erDiagram-INFDFZHY-DlhPv4az.js} +1 -1
- package/dist/client/assets/{excalidraw-editor-Dj0c0PO6.js → excalidraw-editor-CBEWfuhb.js} +2 -2
- package/dist/client/assets/excalidraw-editor-Dj3i59LW.css +1 -0
- package/dist/client/assets/excalidraw-scene-BL-OtvCd.js +176 -0
- package/dist/client/assets/excalidrawEditor-Ws-fwGOc.js +2 -0
- package/dist/client/assets/{export-page-CUOMczdp.js → export-page-B6xYnwy7.js} +2 -2
- package/dist/client/assets/exportDocument-8JBw0p8O.js +2 -0
- package/dist/client/assets/{exportDocument-CLPYN_gg.css → exportDocument-CtX_4PSk.css} +1 -1
- package/dist/client/assets/{flowDiagram-PKNHOUZH-CRbK5_5h.js → flowDiagram-PKNHOUZH-BU4MOSq8.js} +1 -1
- package/dist/client/assets/ganttDiagram-A5KZAMGK-CbX67lZu.js +292 -0
- package/dist/client/assets/gitGraphDiagram-K3NZZRJ6-DhyvWcW1.js +65 -0
- package/dist/client/assets/image-XLNPCCAD-B4wZgh-Y.js +1 -0
- package/dist/client/assets/{index-CVgVzuh6.js → index-CywRKxsk.js} +2 -2
- package/dist/client/assets/index-R4OArFku.css +1 -0
- package/dist/client/assets/infoDiagram-LFFYTUFH-Bap6MyHX.js +2 -0
- package/dist/client/assets/{ishikawaDiagram-PHBUUO56-BDVPgkv4.js → ishikawaDiagram-PHBUUO56-DbftskqN.js} +1 -1
- package/dist/client/assets/{journeyDiagram-4ABVD52K-DtNnwL48.js → journeyDiagram-4ABVD52K-165YoX6B.js} +1 -1
- package/dist/client/assets/{jsx-runtime-B8dyIONw.js → jsx-runtime-DiaMmmG6.js} +1 -1
- package/dist/client/assets/{kanban-definition-K7BYSVSG-C565sUNo.js → kanban-definition-K7BYSVSG-MlixoNyx.js} +1 -1
- package/dist/client/assets/{main-C8adLQnt.js → main-CB97rY6B.js} +172 -133
- package/dist/client/assets/mermaid-parser.core-lf87rdG-.js +4 -0
- package/dist/client/assets/{mermaid-runtime-DqncQoYK.js → mermaid-runtime-C4a7TrTS.js} +3 -3
- package/dist/client/assets/{mindmap-definition-YRQLILUH-BbYzGfTD.js → mindmap-definition-YRQLILUH-_YbQSc4Q.js} +1 -1
- package/dist/client/assets/pieDiagram-SKSYHLDU-CqCgXFom.js +30 -0
- package/dist/client/assets/preview-render-compiler-TmHpBDlh.js +1 -0
- package/dist/client/assets/preview-render-compiler-aYpnXmNm.js +35 -0
- package/dist/client/assets/preview-render-worker-BVbVXXPS.js +59 -0
- package/dist/client/assets/{quadrantDiagram-337W2JSQ-DhlKLozd.js → quadrantDiagram-337W2JSQ-B8eMuhg4.js} +1 -1
- package/dist/client/assets/{requirementDiagram-Z7DCOOCP-B7ZOMsIb.js → requirementDiagram-Z7DCOOCP-B6POvqz4.js} +1 -1
- package/dist/client/assets/{room-B0UGbzQj.js → room-COWSP--i.js} +1 -1
- package/dist/client/assets/stateDiagram-RAJIS63D-DbewhqwJ.js +1 -0
- package/dist/client/assets/stateDiagram-v2-FVOUBMTO-DbW-jVkS.js +1 -0
- package/dist/client/assets/{vennDiagram-LZ73GAT5-Cs9aQmVM.js → vennDiagram-LZ73GAT5-C1hrSbf2.js} +1 -1
- package/dist/client/assets/{xychartDiagram-JWTSCODW-B40ybbzC.js → xychartDiagram-JWTSCODW-DhpWp8LV.js} +1 -1
- package/dist/client/assets/{yjs-provider-reset-guard-DUP9XupO.js → yjs-provider-reset-guard-DWGhQClh.js} +1 -1
- package/dist/client/drawio-editor.html +1 -1
- package/dist/client/excalidraw-editor.html +1 -1
- package/dist/client/export-document.html +4 -4
- package/dist/client/index.html +4 -4
- package/docker-compose.yml +1 -0
- package/package.json +23 -21
- package/src/client/application/app-shell/ui-feature-shell.js +47 -1
- package/src/client/application/app-shell/ui-feature-toolbar.js +25 -3
- package/src/client/application/app-shell-elements.js +2 -0
- package/src/client/application/diagram-preview-hydrator.js +1 -1
- package/src/client/application/markdown-frontmatter.js +152 -0
- package/src/client/application/preview-render-compiler.js +10 -1
- package/src/client/application/preview-render-executor.js +8 -1
- package/src/client/application/preview-render-worker.js +4 -0
- package/src/client/application/preview-renderer.js +51 -1
- package/src/client/application/workspace-preview-controller.js +1 -0
- package/src/client/bootstrap/collabmd-app-shell.js +5 -0
- package/src/client/domain/markdown-formatting.js +1 -1
- package/src/client/domain/markdown-toolbar-actions.js +36 -0
- package/src/client/export/export-pipeline.js +3 -1
- package/src/client/infrastructure/editor-session.js +4 -0
- package/src/client/infrastructure/editor-view-adapter.js +36 -2
- package/src/client/infrastructure/excalidraw-room-client.js +12 -4
- package/src/client/presentation/app-shell-template.js +15 -0
- package/src/client/presentation/file-action-controller.js +1 -2
- package/src/client/presentation/git-diff-view-controller.js +7 -10
- package/src/client/styles/base.css +5 -0
- package/src/client/styles/features/preview-markdown.css +138 -0
- package/src/client/styles/layout/editor-page.css +6 -0
- package/src/client/styles/layout/responsive.css +89 -0
- package/src/client/styles/layout/shell.css +6 -0
- package/src/client/styles/overrides/codemirror.css +74 -0
- package/src/server/auth/create-auth-service.js +32 -6
- package/src/server/config/env.js +27 -0
- package/src/server/config/perf-logging.js +39 -0
- package/src/server/create-app-server.js +102 -5
- package/src/server/domain/backlink-index.js +31 -7
- package/src/server/domain/collaboration/collaboration-room.js +28 -5
- package/src/server/infrastructure/git/history-service.js +1 -1
- package/src/server/infrastructure/persistence/pull-backup-store.js +1 -1
- package/src/server/infrastructure/persistence/vault-file-store.js +74 -31
- package/src/server/infrastructure/workspace/file-system-sync-service.js +46 -12
- package/src/server/infrastructure/workspace/workspace-mutation-coordinator.js +32 -7
- package/src/server/shared/async-utils.js +25 -0
- package/dist/client/assets/architectureDiagram-2XIMDMQ5-Bhh8YQHn.js +0 -36
- package/dist/client/assets/base-B5gONpJK.css +0 -1
- package/dist/client/assets/chunk-27N2SPOM-DbHKsQ_j.js +0 -34
- package/dist/client/assets/classDiagram-VBA2DB6C-Bpdym3dj.js +0 -1
- package/dist/client/assets/classDiagram-v2-RAHNMMFH-CRAsdVDB.js +0 -1
- package/dist/client/assets/dagre-KLK3FWXG-CXSnjrX-.js +0 -4
- package/dist/client/assets/diagram-E7M64L7V-De0StFef.js +0 -24
- package/dist/client/assets/diagram-IFDJBPK2-B2m6AJbU.js +0 -43
- package/dist/client/assets/diagram-P4PSJMXO-YBWLVENw.js +0 -24
- package/dist/client/assets/dist-BUEzOS23.js +0 -1
- package/dist/client/assets/dist-BflL-_8D.js +0 -1
- package/dist/client/assets/dist-CI_qEj5K.js +0 -1
- package/dist/client/assets/dist-CtFNDttZ.js +0 -1
- package/dist/client/assets/dist-DS_4VpEd.js +0 -1
- package/dist/client/assets/dist-DwsFUbuB.js +0 -1
- package/dist/client/assets/dist-VCYAp958.js +0 -1
- package/dist/client/assets/dist-Yxt8txtM.js +0 -1
- package/dist/client/assets/dist-npxgF_6V.js +0 -1
- package/dist/client/assets/dist-u0zDRMQt.js +0 -1
- package/dist/client/assets/editor-session-DFzBn4RY.js +0 -16
- package/dist/client/assets/excalidraw-editor-Bat1FRoT.css +0 -1
- package/dist/client/assets/excalidraw-scene-CixfM-jL.js +0 -174
- package/dist/client/assets/excalidrawEditor-diK4VITg.js +0 -2
- package/dist/client/assets/exportDocument-BM4_f2n8.js +0 -2
- package/dist/client/assets/ganttDiagram-A5KZAMGK-R-AMIgxR.js +0 -292
- package/dist/client/assets/gitGraphDiagram-K3NZZRJ6-CIPit_Sh.js +0 -65
- package/dist/client/assets/image-32MOCVDX-BB6utPzr.js +0 -1
- package/dist/client/assets/index-BOOhEiLo.css +0 -1
- package/dist/client/assets/infoDiagram-LFFYTUFH-CF8jXFTm.js +0 -2
- package/dist/client/assets/mermaid-parser.core-By_5k2SF.js +0 -4
- package/dist/client/assets/pieDiagram-SKSYHLDU-CAU_gC7U.js +0 -30
- package/dist/client/assets/preview-render-compiler-CVfDHnqM.js +0 -1
- package/dist/client/assets/preview-render-compiler-CapHJDHT.js +0 -1
- package/dist/client/assets/preview-render-worker-wMS710Bv.js +0 -23
- package/dist/client/assets/stateDiagram-RAJIS63D-CPoSrNwn.js +0 -1
- package/dist/client/assets/stateDiagram-v2-FVOUBMTO-5GW0Y21a.js +0 -1
- /package/dist/client/assets/{base-r5Zg2n3k.js → base-h1uscjTi.js} +0 -0
|
@@ -2,6 +2,7 @@ import { createServer } from 'http';
|
|
|
2
2
|
|
|
3
3
|
import { loadConfig } from './config/env.js';
|
|
4
4
|
import { createAuthService } from './auth/create-auth-service.js';
|
|
5
|
+
import { logPerfEvent } from './config/perf-logging.js';
|
|
5
6
|
import { BacklinkIndex } from './domain/backlink-index.js';
|
|
6
7
|
import { CollaborationDocumentStore } from './domain/collaboration/collaboration-document-store.js';
|
|
7
8
|
import { CollaborationRoom } from './domain/collaboration/collaboration-room.js';
|
|
@@ -38,6 +39,36 @@ function closeHttpServer(httpServer) {
|
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
function workspaceStateMetadataEqual(left = null, right = null) {
|
|
43
|
+
const leftMetadata = left?.metadata;
|
|
44
|
+
const rightMetadata = right?.metadata;
|
|
45
|
+
if (!(leftMetadata instanceof Map) || !(rightMetadata instanceof Map)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (leftMetadata.size !== rightMetadata.size) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const [pathValue, leftEntry] of leftMetadata.entries()) {
|
|
54
|
+
const rightEntry = rightMetadata.get(pathValue);
|
|
55
|
+
if (!rightEntry) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
leftEntry.type !== rightEntry.type
|
|
61
|
+
|| leftEntry.size !== rightEntry.size
|
|
62
|
+
|| leftEntry.inode !== rightEntry.inode
|
|
63
|
+
|| leftEntry.mtimeMs !== rightEntry.mtimeMs
|
|
64
|
+
) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
41
72
|
export function createAppServer(config = loadConfig()) {
|
|
42
73
|
const authService = createAuthService(config);
|
|
43
74
|
const vaultFileStore = new VaultFileStore({ vaultDir: config.vaultDir });
|
|
@@ -69,6 +100,7 @@ export function createAppServer(config = loadConfig()) {
|
|
|
69
100
|
maxBufferedAmountBytes: config.wsMaxBufferedAmountBytes,
|
|
70
101
|
name,
|
|
71
102
|
onEmpty,
|
|
103
|
+
perfLoggingEnabled: config.perfLoggingEnabled,
|
|
72
104
|
});
|
|
73
105
|
|
|
74
106
|
if (name === WORKSPACE_ROOM_NAME && workspaceMutationCoordinator?.workspaceState) {
|
|
@@ -88,6 +120,7 @@ export function createAppServer(config = loadConfig()) {
|
|
|
88
120
|
vaultFileStore.setManagedWriteTracker(workspaceMutationCoordinator);
|
|
89
121
|
const fileSystemSyncService = new FileSystemSyncService({
|
|
90
122
|
mutationCoordinator: workspaceMutationCoordinator,
|
|
123
|
+
perfLoggingEnabled: config.perfLoggingEnabled,
|
|
91
124
|
vaultFileStore,
|
|
92
125
|
});
|
|
93
126
|
const requestHandler = createRequestHandler(
|
|
@@ -129,21 +162,84 @@ export function createAppServer(config = loadConfig()) {
|
|
|
129
162
|
let vaultFileCount = 0;
|
|
130
163
|
|
|
131
164
|
async function listen() {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
await
|
|
165
|
+
const startupStartedAt = Date.now();
|
|
166
|
+
|
|
167
|
+
const initialWorkspaceScanStartedAt = Date.now();
|
|
168
|
+
const initialWorkspaceSnapshot = await vaultFileStore.scanWorkspaceState();
|
|
169
|
+
vaultFileCount = initialWorkspaceSnapshot.vaultFileCount ?? 0;
|
|
170
|
+
logPerfEvent(config.perfLoggingEnabled, 'startup', {
|
|
171
|
+
durationMs: Date.now() - initialWorkspaceScanStartedAt,
|
|
172
|
+
phase: 'workspace-scan',
|
|
173
|
+
vaultFileCount,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const backlinkBuildStartedAt = Date.now();
|
|
177
|
+
await backlinkIndex.build({ workspaceState: initialWorkspaceSnapshot });
|
|
178
|
+
logPerfEvent(config.perfLoggingEnabled, 'startup', {
|
|
179
|
+
durationMs: Date.now() - backlinkBuildStartedAt,
|
|
180
|
+
markdownFileCount: initialWorkspaceSnapshot.markdownPaths?.length ?? 0,
|
|
181
|
+
phase: 'backlink-build',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const liveWorkspaceScanStartedAt = Date.now();
|
|
185
|
+
const liveWorkspaceSnapshot = await vaultFileStore.scanWorkspaceState();
|
|
186
|
+
const workspaceChangedDuringStartup = !workspaceStateMetadataEqual(initialWorkspaceSnapshot, liveWorkspaceSnapshot);
|
|
187
|
+
vaultFileCount = liveWorkspaceSnapshot.vaultFileCount ?? vaultFileCount;
|
|
188
|
+
logPerfEvent(config.perfLoggingEnabled, 'startup', {
|
|
189
|
+
changedDuringStartup: workspaceChangedDuringStartup,
|
|
190
|
+
durationMs: Date.now() - liveWorkspaceScanStartedAt,
|
|
191
|
+
phase: 'workspace-rescan',
|
|
192
|
+
vaultFileCount,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (workspaceChangedDuringStartup) {
|
|
196
|
+
const backlinkRebuildStartedAt = Date.now();
|
|
197
|
+
await backlinkIndex.build({ workspaceState: liveWorkspaceSnapshot });
|
|
198
|
+
logPerfEvent(config.perfLoggingEnabled, 'startup', {
|
|
199
|
+
durationMs: Date.now() - backlinkRebuildStartedAt,
|
|
200
|
+
markdownFileCount: liveWorkspaceSnapshot.markdownPaths?.length ?? 0,
|
|
201
|
+
phase: 'backlink-rebuild',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const workspaceInitStartedAt = Date.now();
|
|
206
|
+
await workspaceMutationCoordinator.initialize({ snapshot: liveWorkspaceSnapshot });
|
|
207
|
+
logPerfEvent(config.perfLoggingEnabled, 'startup', {
|
|
208
|
+
durationMs: Date.now() - workspaceInitStartedAt,
|
|
209
|
+
phase: 'workspace-init',
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const watcherStartStartedAt = Date.now();
|
|
213
|
+
await fileSystemSyncService.start({ snapshot: liveWorkspaceSnapshot });
|
|
214
|
+
logPerfEvent(config.perfLoggingEnabled, 'startup', {
|
|
215
|
+
durationMs: Date.now() - watcherStartStartedAt,
|
|
216
|
+
phase: 'watcher-start',
|
|
217
|
+
});
|
|
136
218
|
|
|
137
219
|
return new Promise((resolve, reject) => {
|
|
220
|
+
const listenStartedAt = Date.now();
|
|
138
221
|
httpServer.once('error', reject);
|
|
139
222
|
httpServer.listen(config.port, config.host, () => {
|
|
140
223
|
httpServer.off('error', reject);
|
|
141
224
|
const address = httpServer.address();
|
|
142
|
-
|
|
225
|
+
const result = {
|
|
143
226
|
address,
|
|
144
227
|
host: getDisplayHost(config.host),
|
|
145
228
|
port: typeof address === 'object' && address ? address.port : config.port,
|
|
146
229
|
wsPath: `${config.basePath || ''}${config.wsBasePath}/:file`,
|
|
230
|
+
};
|
|
231
|
+
logPerfEvent(config.perfLoggingEnabled, 'startup', {
|
|
232
|
+
durationMs: Date.now() - listenStartedAt,
|
|
233
|
+
phase: 'listen',
|
|
234
|
+
port: result.port,
|
|
235
|
+
});
|
|
236
|
+
logPerfEvent(config.perfLoggingEnabled, 'startup-total', {
|
|
237
|
+
durationMs: Date.now() - startupStartedAt,
|
|
238
|
+
markdownFileCount: liveWorkspaceSnapshot.markdownPaths?.length ?? 0,
|
|
239
|
+
vaultFileCount,
|
|
240
|
+
});
|
|
241
|
+
resolve({
|
|
242
|
+
...result,
|
|
147
243
|
});
|
|
148
244
|
});
|
|
149
245
|
});
|
|
@@ -176,6 +272,7 @@ export function createAppServer(config = loadConfig()) {
|
|
|
176
272
|
roomRegistry,
|
|
177
273
|
workspaceMutationCoordinator,
|
|
178
274
|
authService,
|
|
275
|
+
backlinkIndex,
|
|
179
276
|
fileSystemSyncService,
|
|
180
277
|
gitService,
|
|
181
278
|
setTestHydrateDelayMs(delayMs = 0) {
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { createWikiTargetIndex, resolveWikiTargetWithIndex } from '../../domain/wiki-link-resolver.js';
|
|
14
|
+
import { mapWithConcurrency } from '../shared/async-utils.js';
|
|
14
15
|
|
|
15
16
|
const WIKI_LINK_RE = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
|
|
17
|
+
const BACKLINK_BUILD_CONCURRENCY = 8;
|
|
16
18
|
|
|
17
19
|
function createDeferred() {
|
|
18
20
|
let resolve;
|
|
@@ -58,13 +60,15 @@ export class BacklinkIndex {
|
|
|
58
60
|
* Build the full index by scanning every markdown file in the vault.
|
|
59
61
|
* Called once at server startup.
|
|
60
62
|
*/
|
|
61
|
-
async build() {
|
|
63
|
+
async build({ workspaceState = null } = {}) {
|
|
62
64
|
this._requestedBuildVersion += 1;
|
|
65
|
+
this._latestRequestedWorkspaceState = workspaceState;
|
|
63
66
|
return this.flushScheduledBuild();
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
scheduleBuild({ delayMs = this.rebuildDelayMs } = {}) {
|
|
67
70
|
this._requestedBuildVersion += 1;
|
|
71
|
+
this._latestRequestedWorkspaceState = null;
|
|
68
72
|
if (!this._scheduledBuildDeferred) {
|
|
69
73
|
this._scheduledBuildDeferred = createDeferred();
|
|
70
74
|
}
|
|
@@ -116,8 +120,9 @@ export class BacklinkIndex {
|
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
const targetVersion = this._requestedBuildVersion;
|
|
123
|
+
const workspaceState = this._latestRequestedWorkspaceState;
|
|
119
124
|
this._buildPromise = (async () => {
|
|
120
|
-
await this._performBuild();
|
|
125
|
+
await this._performBuild(workspaceState);
|
|
121
126
|
this._completedBuildVersion = Math.max(this._completedBuildVersion, targetVersion);
|
|
122
127
|
})().finally(() => {
|
|
123
128
|
this._buildPromise = null;
|
|
@@ -126,18 +131,26 @@ export class BacklinkIndex {
|
|
|
126
131
|
return this._buildPromise;
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
async _performBuild() {
|
|
134
|
+
async _performBuild(workspaceState = null) {
|
|
130
135
|
this.forward.clear();
|
|
131
136
|
this.reverse.clear();
|
|
132
137
|
this.contextsBySource.clear();
|
|
133
138
|
|
|
134
|
-
const
|
|
135
|
-
this._fileList =
|
|
139
|
+
const snapshot = workspaceState ?? await this._resolveWorkspaceState();
|
|
140
|
+
this._fileList = Array.from(snapshot?.markdownPaths ?? []);
|
|
136
141
|
this._fileSet = new Set(this._fileList);
|
|
137
142
|
this._refreshWikiTargetIndex();
|
|
138
143
|
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
const fileContents = await mapWithConcurrency(
|
|
145
|
+
this._fileList,
|
|
146
|
+
BACKLINK_BUILD_CONCURRENCY,
|
|
147
|
+
async (filePath) => ({
|
|
148
|
+
content: await this.vaultFileStore.readMarkdownFile(filePath),
|
|
149
|
+
filePath,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
for (const { content, filePath } of fileContents) {
|
|
141
154
|
if (content !== null) {
|
|
142
155
|
this._indexFile(filePath, content);
|
|
143
156
|
}
|
|
@@ -147,6 +160,17 @@ export class BacklinkIndex {
|
|
|
147
160
|
console.log(`[backlinks] Index built: ${this._fileList.length} files, ${this.reverse.size} targets with backlinks`);
|
|
148
161
|
}
|
|
149
162
|
|
|
163
|
+
async _resolveWorkspaceState() {
|
|
164
|
+
if (typeof this.vaultFileStore.scanWorkspaceState === 'function') {
|
|
165
|
+
return this.vaultFileStore.scanWorkspaceState();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const tree = await this.vaultFileStore.tree();
|
|
169
|
+
return {
|
|
170
|
+
markdownPaths: flattenTree(tree),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
150
174
|
/**
|
|
151
175
|
* Incrementally update the index when a file's content changes.
|
|
152
176
|
* Call this after every persist / write.
|
|
@@ -8,6 +8,7 @@ import { MSG_AWARENESS, MSG_SYNC } from './protocol.js';
|
|
|
8
8
|
import { CollaborationDocumentStore } from './collaboration-document-store.js';
|
|
9
9
|
import { RoomClientStateStore } from './room-client-state-store.js';
|
|
10
10
|
import { RoomPersistenceController } from './room-persistence-controller.js';
|
|
11
|
+
import { logPerfEvent } from '../../config/perf-logging.js';
|
|
11
12
|
import { populateCommentThreads, serializeCommentThreads } from '../../../domain/comment-threads.js';
|
|
12
13
|
import {
|
|
13
14
|
isExcalidrawRoomDocStructured,
|
|
@@ -131,6 +132,7 @@ export class CollaborationRoom {
|
|
|
131
132
|
name,
|
|
132
133
|
idleGraceMs = 0,
|
|
133
134
|
maxBufferedAmountBytes,
|
|
135
|
+
perfLoggingEnabled = false,
|
|
134
136
|
vaultFileStore,
|
|
135
137
|
backlinkIndex,
|
|
136
138
|
onEmpty,
|
|
@@ -138,6 +140,7 @@ export class CollaborationRoom {
|
|
|
138
140
|
this.name = name;
|
|
139
141
|
this.idleGraceMs = idleGraceMs;
|
|
140
142
|
this.maxBufferedAmountBytes = maxBufferedAmountBytes;
|
|
143
|
+
this.perfLoggingEnabled = perfLoggingEnabled;
|
|
141
144
|
this.documentStore = documentStore ?? new CollaborationDocumentStore({
|
|
142
145
|
backlinkIndex,
|
|
143
146
|
name,
|
|
@@ -187,6 +190,8 @@ export class CollaborationRoom {
|
|
|
187
190
|
let snapshotExists = false;
|
|
188
191
|
let snapshotValid = false;
|
|
189
192
|
let commentThreadCount = 0;
|
|
193
|
+
let snapshotReadDurationMs = 0;
|
|
194
|
+
let contentFallbackDurationMs = 0;
|
|
190
195
|
const hydrateDelayMs = Math.max(0, Number(this.getHydrateDelayMs?.() || 0));
|
|
191
196
|
|
|
192
197
|
try {
|
|
@@ -195,7 +200,9 @@ export class CollaborationRoom {
|
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
if (this.documentStore?.hasPersistence()) {
|
|
203
|
+
const snapshotReadStartedAt = Date.now();
|
|
198
204
|
const snapshot = await this.documentStore.readSnapshot();
|
|
205
|
+
snapshotReadDurationMs = Date.now() - snapshotReadStartedAt;
|
|
199
206
|
if (snapshot) {
|
|
200
207
|
snapshotExists = true;
|
|
201
208
|
try {
|
|
@@ -227,10 +234,12 @@ export class CollaborationRoom {
|
|
|
227
234
|
}
|
|
228
235
|
}
|
|
229
236
|
|
|
237
|
+
const fallbackStartedAt = Date.now();
|
|
230
238
|
const [content, commentThreads] = await Promise.all([
|
|
231
239
|
this.documentStore.readContent(),
|
|
232
240
|
this.documentStore.readCommentThreads(),
|
|
233
241
|
]);
|
|
242
|
+
contentFallbackDurationMs = Date.now() - fallbackStartedAt;
|
|
234
243
|
commentThreadCount = commentThreads.length;
|
|
235
244
|
if (content !== null || commentThreads.length > 0) {
|
|
236
245
|
hydrateSource = 'content';
|
|
@@ -262,14 +271,23 @@ export class CollaborationRoom {
|
|
|
262
271
|
this.debugMetrics.hydrateCount += 1;
|
|
263
272
|
this.debugMetrics.lastHydrate = {
|
|
264
273
|
commentThreadCount,
|
|
274
|
+
contentFallbackDurationMs,
|
|
265
275
|
durationMs: Date.now() - startedAt,
|
|
276
|
+
snapshotReadDurationMs,
|
|
266
277
|
snapshotExists,
|
|
267
278
|
snapshotValid,
|
|
268
279
|
source: hydrateSource,
|
|
269
280
|
};
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
281
|
+
logPerfEvent(this.perfLoggingEnabled, `room:${this.name}`, {
|
|
282
|
+
commentThreads: this.debugMetrics.lastHydrate.commentThreadCount,
|
|
283
|
+
contentFallbackDurationMs: this.debugMetrics.lastHydrate.contentFallbackDurationMs,
|
|
284
|
+
durationMs: this.debugMetrics.lastHydrate.durationMs,
|
|
285
|
+
event: 'hydrate',
|
|
286
|
+
snapshotExists: this.debugMetrics.lastHydrate.snapshotExists,
|
|
287
|
+
snapshotReadDurationMs: this.debugMetrics.lastHydrate.snapshotReadDurationMs,
|
|
288
|
+
snapshotValid: this.debugMetrics.lastHydrate.snapshotValid,
|
|
289
|
+
source: this.debugMetrics.lastHydrate.source,
|
|
290
|
+
});
|
|
273
291
|
}
|
|
274
292
|
}
|
|
275
293
|
})().catch((error) => {
|
|
@@ -522,8 +540,13 @@ export class CollaborationRoom {
|
|
|
522
540
|
sendInitialSync(ws) {
|
|
523
541
|
this.debugMetrics.initialSyncCount += 1;
|
|
524
542
|
this.debugMetrics.lastInitialSyncAt = Date.now();
|
|
525
|
-
|
|
526
|
-
|
|
543
|
+
const initialSyncMessage = this.getInitialSyncMessage();
|
|
544
|
+
logPerfEvent(this.perfLoggingEnabled, `room:${this.name}`, {
|
|
545
|
+
bytes: initialSyncMessage.byteLength,
|
|
546
|
+
count: this.debugMetrics.initialSyncCount,
|
|
547
|
+
event: 'initial-sync',
|
|
548
|
+
});
|
|
549
|
+
return sendMessage.call(this, ws, initialSyncMessage, this);
|
|
527
550
|
}
|
|
528
551
|
|
|
529
552
|
async addClient(ws, { sendInitialSync: shouldSendInitialSync = true } = {}) {
|
|
@@ -430,7 +430,7 @@ export class GitHistoryService {
|
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
return this.runRequest(cacheKey, async () => {
|
|
433
|
-
let content
|
|
433
|
+
let content;
|
|
434
434
|
try {
|
|
435
435
|
content = await this.commandRunner.execGit(['show', `${normalizedHash}:${normalizedPath}`]);
|
|
436
436
|
} catch {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isMarkdownFilePath,
|
|
8
8
|
isVaultFilePath,
|
|
9
9
|
} from '../../../domain/file-kind.js';
|
|
10
|
+
import { mapWithConcurrency } from '../../shared/async-utils.js';
|
|
10
11
|
import { getVaultContentAdapter } from './vault-content-adapter.js';
|
|
11
12
|
import {
|
|
12
13
|
INVALID_VAULT_FILE_PATH_ERROR,
|
|
@@ -36,6 +37,7 @@ const MIME_TYPE_TO_IMAGE_EXTENSION = Object.freeze({
|
|
|
36
37
|
'image/svg+xml': '.svg',
|
|
37
38
|
'image/webp': '.webp',
|
|
38
39
|
});
|
|
40
|
+
const WORKSPACE_SCAN_CONCURRENCY = 8;
|
|
39
41
|
|
|
40
42
|
function createTransactionalPath(targetPath, label) {
|
|
41
43
|
return `${targetPath}.collabmd-${label}-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
@@ -74,6 +76,16 @@ function createWorkspaceEntry(relativePath, type) {
|
|
|
74
76
|
};
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
function createWorkspaceMetadata(pathValue, type, info) {
|
|
80
|
+
return {
|
|
81
|
+
inode: Number(info.ino || 0),
|
|
82
|
+
mtimeMs: Number(info.mtimeMs || 0),
|
|
83
|
+
path: pathValue,
|
|
84
|
+
size: type === 'directory' ? 0 : Number(info.size || 0),
|
|
85
|
+
type,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
77
89
|
function replacePathPrefix(pathValue, oldPrefix, newPrefix) {
|
|
78
90
|
if (pathValue === oldPrefix) {
|
|
79
91
|
return newPrefix;
|
|
@@ -94,6 +106,14 @@ function sortWorkspacePaths(paths = [], direction = 'asc') {
|
|
|
94
106
|
});
|
|
95
107
|
}
|
|
96
108
|
|
|
109
|
+
function sortDirectoryEntries(entries = []) {
|
|
110
|
+
return [...entries].sort((left, right) => {
|
|
111
|
+
if (left.isDirectory() && !right.isDirectory()) return -1;
|
|
112
|
+
if (!left.isDirectory() && right.isDirectory()) return 1;
|
|
113
|
+
return left.name.localeCompare(right.name, undefined, { sensitivity: 'base' });
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
97
117
|
async function cleanupPaths(paths = []) {
|
|
98
118
|
await Promise.allSettled(paths.filter(Boolean).map((pathValue) => rm(pathValue, { force: true })));
|
|
99
119
|
}
|
|
@@ -398,8 +418,8 @@ export class VaultFileStore {
|
|
|
398
418
|
const timestamp = createAttachmentTimestamp(now);
|
|
399
419
|
const baseFileName = `${attachmentStem}-${timestamp}`;
|
|
400
420
|
let collisionIndex = 0;
|
|
401
|
-
let storedPath
|
|
402
|
-
let absolutePath
|
|
421
|
+
let storedPath;
|
|
422
|
+
let absolutePath;
|
|
403
423
|
|
|
404
424
|
do {
|
|
405
425
|
const suffix = collisionIndex > 0 ? `-${collisionIndex + 1}` : '';
|
|
@@ -765,7 +785,8 @@ export class VaultFileStore {
|
|
|
765
785
|
}
|
|
766
786
|
|
|
767
787
|
async countVaultFiles() {
|
|
768
|
-
|
|
788
|
+
const snapshot = await this.scanWorkspaceState();
|
|
789
|
+
return snapshot.vaultFileCount;
|
|
769
790
|
}
|
|
770
791
|
|
|
771
792
|
async reconcileSidecars({
|
|
@@ -838,6 +859,8 @@ export class VaultFileStore {
|
|
|
838
859
|
async scanWorkspaceState() {
|
|
839
860
|
const entries = new Map();
|
|
840
861
|
const metadata = new Map();
|
|
862
|
+
const markdownPaths = [];
|
|
863
|
+
let vaultFileCount = 0;
|
|
841
864
|
|
|
842
865
|
const visitDirectory = async (dirPath) => {
|
|
843
866
|
let dirEntries;
|
|
@@ -847,64 +870,84 @@ export class VaultFileStore {
|
|
|
847
870
|
return;
|
|
848
871
|
}
|
|
849
872
|
|
|
850
|
-
const sorted = dirEntries
|
|
851
|
-
if (left.isDirectory() && !right.isDirectory()) return -1;
|
|
852
|
-
if (!left.isDirectory() && right.isDirectory()) return 1;
|
|
853
|
-
return left.name.localeCompare(right.name, undefined, { sensitivity: 'base' });
|
|
854
|
-
});
|
|
873
|
+
const sorted = sortDirectoryEntries(dirEntries);
|
|
855
874
|
|
|
856
|
-
|
|
875
|
+
await mapWithConcurrency(sorted, WORKSPACE_SCAN_CONCURRENCY, async (entry) => {
|
|
857
876
|
if (isIgnoredVaultEntry(entry.name)) {
|
|
858
|
-
|
|
877
|
+
return;
|
|
859
878
|
}
|
|
860
879
|
|
|
861
880
|
const fullPath = join(dirPath, entry.name);
|
|
862
881
|
const relativePath = toVaultRelativePath(this.vaultDir, fullPath).replace(/\\/g, '/');
|
|
882
|
+
const direntKind = entry.isDirectory()
|
|
883
|
+
? 'directory'
|
|
884
|
+
: (entry.isFile() ? 'file' : null);
|
|
863
885
|
|
|
864
|
-
if (
|
|
886
|
+
if (direntKind === 'directory') {
|
|
865
887
|
entries.set(relativePath, createWorkspaceEntry(relativePath, 'directory'));
|
|
866
888
|
try {
|
|
867
889
|
const info = await stat(fullPath);
|
|
868
|
-
metadata.set(relativePath,
|
|
869
|
-
inode: Number(info.ino || 0),
|
|
870
|
-
mtimeMs: Number(info.mtimeMs || 0),
|
|
871
|
-
path: relativePath,
|
|
872
|
-
size: 0,
|
|
873
|
-
type: 'directory',
|
|
874
|
-
});
|
|
890
|
+
metadata.set(relativePath, createWorkspaceMetadata(relativePath, 'directory', info));
|
|
875
891
|
} catch {
|
|
876
892
|
// Ignore transient directories that disappear during scans.
|
|
877
893
|
}
|
|
878
894
|
await visitDirectory(fullPath);
|
|
879
|
-
|
|
895
|
+
return;
|
|
880
896
|
}
|
|
881
897
|
|
|
882
|
-
if (
|
|
883
|
-
|
|
898
|
+
if (direntKind === 'file') {
|
|
899
|
+
if (!isVaultFilePath(entry.name)) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
entries.set(relativePath, createWorkspaceEntry(relativePath, 'file'));
|
|
904
|
+
vaultFileCount += 1;
|
|
905
|
+
if (isMarkdownFilePath(relativePath)) {
|
|
906
|
+
markdownPaths.push(relativePath);
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
const info = await stat(fullPath);
|
|
910
|
+
metadata.set(relativePath, createWorkspaceMetadata(relativePath, 'file', info));
|
|
911
|
+
} catch {
|
|
912
|
+
// Ignore transient files that disappear during scans.
|
|
913
|
+
}
|
|
914
|
+
return;
|
|
884
915
|
}
|
|
885
916
|
|
|
886
|
-
entries.set(relativePath, createWorkspaceEntry(relativePath, 'file'));
|
|
887
917
|
try {
|
|
888
918
|
const info = await stat(fullPath);
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
919
|
+
if (info.isDirectory()) {
|
|
920
|
+
entries.set(relativePath, createWorkspaceEntry(relativePath, 'directory'));
|
|
921
|
+
metadata.set(relativePath, createWorkspaceMetadata(relativePath, 'directory', info));
|
|
922
|
+
await visitDirectory(fullPath);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (!info.isFile() || !isVaultFilePath(entry.name)) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
entries.set(relativePath, createWorkspaceEntry(relativePath, 'file'));
|
|
931
|
+
metadata.set(relativePath, createWorkspaceMetadata(relativePath, 'file', info));
|
|
932
|
+
vaultFileCount += 1;
|
|
933
|
+
if (isMarkdownFilePath(relativePath)) {
|
|
934
|
+
markdownPaths.push(relativePath);
|
|
935
|
+
}
|
|
896
936
|
} catch {
|
|
897
|
-
// Ignore transient
|
|
937
|
+
// Ignore transient entries that disappear during scans.
|
|
898
938
|
}
|
|
899
|
-
}
|
|
939
|
+
});
|
|
900
940
|
};
|
|
901
941
|
|
|
902
942
|
await visitDirectory(this.vaultDir);
|
|
943
|
+
markdownPaths.sort((left, right) => left.localeCompare(right, undefined, { sensitivity: 'base' }));
|
|
903
944
|
|
|
904
945
|
return {
|
|
905
946
|
entries,
|
|
906
947
|
metadata,
|
|
948
|
+
markdownPaths,
|
|
907
949
|
scannedAt: Date.now(),
|
|
950
|
+
vaultFileCount,
|
|
908
951
|
};
|
|
909
952
|
}
|
|
910
953
|
}
|