collabmd 0.1.20 → 0.1.23
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 +98 -9
- package/bin/collabmd.js +3 -2
- package/docker-compose.yml +13 -1
- package/package.json +2 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/{chunk-HEWVH67U.js → chunk-CYK7KO7W.js} +8 -8
- package/public/assets/js/chunks/chunk-GFDM7YCF.js +1 -0
- package/public/assets/js/chunks/editor-session-S5AT7BMV.js +22 -0
- package/public/assets/js/chunks/{preview-render-compiler-UZ4SZDQQ.js → preview-render-compiler-6Y7TKXFJ.js} +1 -1
- package/public/assets/js/chunks/{quick-switcher-controller-J7I3CUET.js → quick-switcher-controller-A6SKH5HI.js} +1 -1
- package/public/assets/js/{excalidraw-editor-ZDYKMZOL.js → excalidraw-editor-5YWMGLKF.js} +145 -62
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +503 -182
- package/public/assets/js/preview-render-worker.js +15 -15
- package/public/index.html +104 -84
- package/src/client/application/app-shell/git-feature.js +304 -27
- package/src/client/application/app-shell/ui-feature.js +69 -7
- package/src/client/application/app-shell/workspace-feature.js +46 -0
- package/src/client/application/app-shell-elements.js +2 -0
- package/src/client/application/mermaid-preview-hydrator.js +1 -1
- package/src/client/application/plantuml-preview-hydrator.js +1 -1
- package/src/client/application/workspace-coordinator.js +147 -18
- package/src/client/application/workspace-preview-controller.js +34 -0
- package/src/client/application/workspace-route-controller.js +104 -0
- package/src/client/bootstrap/collabmd-app-shell.js +107 -0
- package/src/client/domain/runtime-paths.js +5 -0
- package/src/client/domain/vault-utils.js +2 -2
- package/src/client/excalidraw-editor.js +52 -3
- package/src/client/infrastructure/auth-client.js +219 -52
- package/src/client/infrastructure/browser-navigation-port.js +20 -0
- package/src/client/infrastructure/editor-collaboration-client.js +4 -0
- package/src/client/infrastructure/editor-session.js +51 -7
- package/src/client/infrastructure/editor-view-adapter.js +297 -53
- package/src/client/infrastructure/runtime-config.js +66 -0
- package/src/client/infrastructure/workspace-sync-client.js +324 -0
- package/src/client/presentation/comment-ui-controller.js +274 -31
- package/src/client/presentation/comments-panel.js +2 -2
- package/src/client/presentation/excalidraw-embed-controller.js +60 -2
- package/src/client/presentation/file-explorer-controller.js +15 -3
- package/src/client/presentation/file-explorer-view.js +152 -11
- package/src/client/presentation/file-history-view-controller.js +806 -0
- package/src/client/presentation/git-diff-view-controller.js +718 -121
- package/src/client/presentation/git-panel-controller.js +458 -66
- package/src/client/presentation/layout-controller.js +1 -1
- package/src/client/styles/components/actions.css +89 -0
- package/src/client/styles/components/feedback.css +43 -0
- package/src/client/styles/components/navigation.css +68 -0
- package/src/client/styles/components/scrollbars.css +104 -0
- package/src/client/styles/components/states.css +289 -0
- package/src/client/styles/components/surfaces.css +155 -0
- package/src/client/styles/features/backlinks.css +213 -0
- package/src/client/styles/features/collaboration-chat.css +228 -0
- package/src/client/styles/features/collaboration-dialogs.css +154 -0
- package/src/client/styles/features/collaboration-presence.css +103 -0
- package/src/client/styles/features/collaboration-ui.css +3 -0
- package/src/client/styles/features/comment-card.css +236 -0
- package/src/client/styles/features/comment-markdown.css +127 -0
- package/src/client/styles/features/comment-overlays.css +60 -0
- package/src/client/styles/features/comments-drawer.css +120 -0
- package/src/client/styles/features/comments.css +4 -0
- package/src/client/styles/features/diagram-preview.css +227 -0
- package/src/client/styles/features/diff-commit.css +268 -0
- package/src/client/styles/features/diff-lines.css +160 -0
- package/src/client/styles/features/diff-toolbar.css +98 -0
- package/src/client/styles/features/diff.css +3 -0
- package/src/client/styles/features/embedded-preview.css +218 -0
- package/src/client/styles/features/git-branch.css +155 -0
- package/src/client/styles/features/git-files.css +105 -0
- package/src/client/styles/features/git-history.css +339 -0
- package/src/client/styles/features/git.css +3 -0
- package/src/client/styles/features/media-lightbox.css +157 -0
- package/src/client/styles/features/outline.css +115 -0
- package/src/client/styles/features/preview-content.css +3 -0
- package/src/client/styles/features/preview-markdown.css +201 -0
- package/src/client/styles/features/preview-sidebars.css +2 -0
- package/src/client/styles/features/quick-switcher.css +167 -0
- package/src/client/styles/foundation/themes.css +347 -0
- package/src/client/styles/foundation/tokens.css +66 -0
- package/src/client/styles/foundation/utilities.css +36 -0
- package/src/client/styles/layout/editor-page.css +203 -0
- package/src/client/styles/layout/overlays.css +132 -0
- package/src/client/styles/layout/responsive.css +251 -0
- package/src/client/styles/layout/shell.css +314 -0
- package/src/client/styles/layout/sidebar.css +176 -0
- package/src/client/styles/layout/view-modes.css +92 -0
- package/src/client/styles/overrides/codemirror.css +53 -0
- package/src/client/styles/overrides/highlightjs.css +7 -0
- package/src/client/styles/overrides/vendor.css +2 -0
- package/src/client/styles/primitives/controls.css +276 -0
- package/src/client/styles/style.css +25 -4901
- package/src/domain/wiki-link-resolver.js +16 -5
- package/src/domain/workspace-change.js +68 -0
- package/src/domain/workspace-room.js +3 -0
- package/src/server/auth/create-auth-service.js +373 -15
- package/src/server/auth/session-cookie.js +30 -4
- package/src/server/config/env.js +121 -5
- package/src/server/create-app-server.js +49 -10
- package/src/server/domain/backlink-index.js +94 -1
- package/src/server/domain/collaboration/collaboration-room.js +281 -69
- package/src/server/domain/collaboration/room-registry.js +64 -10
- package/src/server/index.js +3 -1
- package/src/server/infrastructure/git/command-runner.js +2 -1
- package/src/server/infrastructure/git/git-service.js +48 -2
- package/src/server/infrastructure/git/history-service.js +683 -0
- package/src/server/infrastructure/git/parsers.js +30 -0
- package/src/server/infrastructure/git/responses.js +85 -19
- package/src/server/infrastructure/http/create-auth-api-handler.js +30 -2
- package/src/server/infrastructure/http/create-git-api-command-handler.js +55 -43
- package/src/server/infrastructure/http/create-git-api-handler.js +4 -0
- package/src/server/infrastructure/http/create-git-api-query-handler.js +61 -0
- package/src/server/infrastructure/http/create-request-handler.js +22 -0
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +58 -14
- package/src/server/infrastructure/http/create-vault-api-handler.js +3 -0
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +3 -1
- package/src/server/infrastructure/http/http-response.js +65 -28
- package/src/server/infrastructure/persistence/vault-file-store.js +163 -52
- package/src/server/infrastructure/workspace/file-system-sync-service.js +488 -0
- package/src/server/infrastructure/workspace/workspace-mutation-coordinator.js +551 -0
- package/public/assets/js/chunks/chunk-R3DDMJHH.js +0 -1
- package/public/assets/js/chunks/editor-session-TAV2VXTA.js +0 -22
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
serializeExcalidrawRoomScene,
|
|
18
18
|
tryParseExcalidrawSceneJson,
|
|
19
19
|
} from '../../../domain/excalidraw-room-codec.js';
|
|
20
|
+
import { normalizeWorkspaceEvent } from '../../../domain/workspace-change.js';
|
|
21
|
+
import { WORKSPACE_EVENT_MAX_MESSAGES, WORKSPACE_ROOM_NAME } from '../../../domain/workspace-room.js';
|
|
20
22
|
|
|
21
23
|
function closeSlowClient(ws, clientState, { maxBufferedAmountBytes, name }) {
|
|
22
24
|
if (!clientState || clientState.backpressureCloseIssued) {
|
|
@@ -86,9 +88,45 @@ function isExcalidrawRoom(name) {
|
|
|
86
88
|
return typeof name === 'string' && name.endsWith('.excalidraw');
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
function isWorkspaceRoom(name) {
|
|
92
|
+
return name === WORKSPACE_ROOM_NAME;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function computeTextReplacement(currentContent, nextContent) {
|
|
96
|
+
const currentText = String(currentContent ?? '');
|
|
97
|
+
const nextText = String(nextContent ?? '');
|
|
98
|
+
if (currentText === nextText) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let prefixLength = 0;
|
|
103
|
+
const maxPrefix = Math.min(currentText.length, nextText.length);
|
|
104
|
+
while (prefixLength < maxPrefix && currentText[prefixLength] === nextText[prefixLength]) {
|
|
105
|
+
prefixLength += 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let currentSuffixLength = currentText.length;
|
|
109
|
+
let nextSuffixLength = nextText.length;
|
|
110
|
+
while (
|
|
111
|
+
currentSuffixLength > prefixLength
|
|
112
|
+
&& nextSuffixLength > prefixLength
|
|
113
|
+
&& currentText[currentSuffixLength - 1] === nextText[nextSuffixLength - 1]
|
|
114
|
+
) {
|
|
115
|
+
currentSuffixLength -= 1;
|
|
116
|
+
nextSuffixLength -= 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
deleteCount: currentSuffixLength - prefixLength,
|
|
121
|
+
insertText: nextText.slice(prefixLength, nextSuffixLength),
|
|
122
|
+
start: prefixLength,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
89
126
|
export class CollaborationRoom {
|
|
90
127
|
constructor({
|
|
91
128
|
documentStore = null,
|
|
129
|
+
getHydrateDelayMs = null,
|
|
92
130
|
name,
|
|
93
131
|
idleGraceMs = 0,
|
|
94
132
|
maxBufferedAmountBytes,
|
|
@@ -104,6 +142,7 @@ export class CollaborationRoom {
|
|
|
104
142
|
name,
|
|
105
143
|
vaultFileStore,
|
|
106
144
|
});
|
|
145
|
+
this.getHydrateDelayMs = getHydrateDelayMs;
|
|
107
146
|
this.onEmpty = onEmpty;
|
|
108
147
|
this.doc = new Y.Doc({ gc: true });
|
|
109
148
|
this.awareness = new awarenessProtocol.Awareness(this.doc);
|
|
@@ -115,6 +154,12 @@ export class CollaborationRoom {
|
|
|
115
154
|
this.destroyed = false;
|
|
116
155
|
this.cachedInitialSyncMessage = null;
|
|
117
156
|
this.activePersistPromise = null;
|
|
157
|
+
this.debugMetrics = {
|
|
158
|
+
hydrateCount: 0,
|
|
159
|
+
initialSyncCount: 0,
|
|
160
|
+
lastHydrate: null,
|
|
161
|
+
lastInitialSyncAt: 0,
|
|
162
|
+
};
|
|
118
163
|
this.persistence = new RoomPersistenceController({
|
|
119
164
|
idleGraceMs: this.idleGraceMs,
|
|
120
165
|
onDestroy: () => {
|
|
@@ -136,64 +181,96 @@ export class CollaborationRoom {
|
|
|
136
181
|
|
|
137
182
|
if (!this.hydratePromise) {
|
|
138
183
|
this.hydratePromise = (async () => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
184
|
+
const startedAt = Date.now();
|
|
185
|
+
let hydrateSource = 'empty';
|
|
186
|
+
let snapshotExists = false;
|
|
187
|
+
let snapshotValid = false;
|
|
188
|
+
let commentThreadCount = 0;
|
|
189
|
+
const hydrateDelayMs = Math.max(0, Number(this.getHydrateDelayMs?.() || 0));
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
if (hydrateDelayMs > 0) {
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, hydrateDelayMs));
|
|
194
|
+
}
|
|
150
195
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
196
|
+
if (this.documentStore?.hasPersistence()) {
|
|
197
|
+
const snapshot = await this.documentStore.readSnapshot();
|
|
198
|
+
if (snapshot) {
|
|
199
|
+
snapshotExists = true;
|
|
200
|
+
try {
|
|
201
|
+
if (isExcalidrawRoom(this.name)) {
|
|
202
|
+
const validationDoc = new Y.Doc({ gc: true });
|
|
203
|
+
try {
|
|
204
|
+
Y.applyUpdate(validationDoc, snapshot, 'hydrate');
|
|
205
|
+
if (!this.ensureStructuredExcalidrawState(validationDoc)) {
|
|
206
|
+
throw new Error('snapshot did not contain a valid structured Excalidraw scene');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
Y.applyUpdate(this.doc, Y.encodeStateAsUpdate(validationDoc), 'hydrate');
|
|
210
|
+
} finally {
|
|
211
|
+
validationDoc.destroy();
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
Y.applyUpdate(this.doc, snapshot, 'hydrate');
|
|
154
215
|
}
|
|
155
|
-
} else {
|
|
156
|
-
Y.applyUpdate(this.doc, snapshot, 'hydrate');
|
|
157
|
-
}
|
|
158
216
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
217
|
+
snapshotValid = true;
|
|
218
|
+
hydrateSource = 'snapshot';
|
|
219
|
+
this.hydrated = true;
|
|
220
|
+
return;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.warn(
|
|
223
|
+
`[room:${this.name}] Discarding invalid collaboration snapshot: ${error.message}`,
|
|
224
|
+
);
|
|
225
|
+
await this.documentStore.deleteSnapshot?.();
|
|
226
|
+
}
|
|
166
227
|
}
|
|
167
|
-
}
|
|
168
228
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
229
|
+
const [content, commentThreads] = await Promise.all([
|
|
230
|
+
this.documentStore.readContent(),
|
|
231
|
+
this.documentStore.readCommentThreads(),
|
|
232
|
+
]);
|
|
233
|
+
commentThreadCount = commentThreads.length;
|
|
234
|
+
if (content !== null || commentThreads.length > 0) {
|
|
235
|
+
hydrateSource = 'content';
|
|
236
|
+
const ytext = this.doc.getText('codemirror');
|
|
237
|
+
const comments = this.doc.getArray('comments');
|
|
238
|
+
this.doc.transact(() => {
|
|
239
|
+
if (isExcalidrawRoom(this.name)) {
|
|
240
|
+
const parsedScene = tryParseExcalidrawSceneJson(content);
|
|
241
|
+
if (parsedScene) {
|
|
242
|
+
migrateLegacyExcalidrawRoomData(this.doc, parsedScene);
|
|
243
|
+
} else if (content !== null) {
|
|
244
|
+
ytext.insert(0, content);
|
|
245
|
+
}
|
|
181
246
|
} else if (content !== null) {
|
|
182
247
|
ytext.insert(0, content);
|
|
183
248
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
249
|
+
populateCommentThreads(comments, commentThreads);
|
|
250
|
+
}, 'hydrate');
|
|
251
|
+
|
|
252
|
+
void this.documentStore.writeSnapshot(Y.encodeStateAsUpdate(this.doc)).catch((error) => {
|
|
253
|
+
console.error(`[room:${this.name}] Failed to prime collaboration snapshot: ${error.message}`);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
189
257
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
258
|
+
this.hydrated = true;
|
|
259
|
+
} finally {
|
|
260
|
+
if (this.hydrated) {
|
|
261
|
+
this.debugMetrics.hydrateCount += 1;
|
|
262
|
+
this.debugMetrics.lastHydrate = {
|
|
263
|
+
commentThreadCount,
|
|
264
|
+
durationMs: Date.now() - startedAt,
|
|
265
|
+
snapshotExists,
|
|
266
|
+
snapshotValid,
|
|
267
|
+
source: hydrateSource,
|
|
268
|
+
};
|
|
269
|
+
console.info(
|
|
270
|
+
`[perf][room:${this.name}] hydrate durationMs=${this.debugMetrics.lastHydrate.durationMs} source=${this.debugMetrics.lastHydrate.source} snapshotExists=${this.debugMetrics.lastHydrate.snapshotExists} snapshotValid=${this.debugMetrics.lastHydrate.snapshotValid} commentThreads=${this.debugMetrics.lastHydrate.commentThreadCount}`,
|
|
271
|
+
);
|
|
193
272
|
}
|
|
194
273
|
}
|
|
195
|
-
|
|
196
|
-
this.hydrated = true;
|
|
197
274
|
})().catch((error) => {
|
|
198
275
|
this.hydratePromise = null;
|
|
199
276
|
console.error(`[room:${this.name}] Failed to hydrate from disk: ${error.message}`);
|
|
@@ -332,36 +409,75 @@ export class CollaborationRoom {
|
|
|
332
409
|
return false;
|
|
333
410
|
}
|
|
334
411
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
412
|
+
return this.applyExternalContent(content, {
|
|
413
|
+
commentThreads,
|
|
414
|
+
replaceCommentThreads: true,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async applyExternalContent(content, {
|
|
419
|
+
commentThreads = [],
|
|
420
|
+
replaceCommentThreads = false,
|
|
421
|
+
} = {}) {
|
|
422
|
+
if (this.deleted || this.destroyed) {
|
|
423
|
+
return { ok: false, reason: 'room-unavailable' };
|
|
342
424
|
}
|
|
343
425
|
|
|
344
|
-
this.doc.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
426
|
+
const comments = this.doc.getArray('comments');
|
|
427
|
+
if (isExcalidrawRoom(this.name)) {
|
|
428
|
+
const parsedExcalidrawScene = tryParseExcalidrawSceneJson(content);
|
|
429
|
+
if (!parsedExcalidrawScene) {
|
|
430
|
+
return { ok: false, reason: 'invalid-excalidraw' };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
this.doc.transact(() => {
|
|
349
434
|
replaceExcalidrawRoomScene(this.doc, parsedExcalidrawScene);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
ytext.insert(0, content);
|
|
435
|
+
if (replaceCommentThreads) {
|
|
436
|
+
if (comments.length > 0) {
|
|
437
|
+
comments.delete(0, comments.length);
|
|
438
|
+
}
|
|
439
|
+
populateCommentThreads(comments, commentThreads);
|
|
356
440
|
}
|
|
441
|
+
}, 'workspace-reconcile');
|
|
442
|
+
|
|
443
|
+
return { ok: true, highlightRange: null };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const ytext = this.doc.getText('codemirror');
|
|
447
|
+
const replacement = computeTextReplacement(ytext.toString(), content);
|
|
448
|
+
if (!replacement && !replaceCommentThreads) {
|
|
449
|
+
return { highlightRange: null, ok: true, skipped: true };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this.doc.transact(() => {
|
|
453
|
+
if (replacement?.deleteCount) {
|
|
454
|
+
ytext.delete(replacement.start, replacement.deleteCount);
|
|
357
455
|
}
|
|
358
|
-
if (
|
|
359
|
-
|
|
456
|
+
if (replacement?.insertText) {
|
|
457
|
+
ytext.insert(replacement.start, replacement.insertText);
|
|
458
|
+
}
|
|
459
|
+
if (replaceCommentThreads) {
|
|
460
|
+
if (comments.length > 0) {
|
|
461
|
+
comments.delete(0, comments.length);
|
|
462
|
+
}
|
|
463
|
+
populateCommentThreads(comments, commentThreads);
|
|
360
464
|
}
|
|
361
|
-
populateCommentThreads(comments, commentThreads);
|
|
362
465
|
}, 'workspace-reconcile');
|
|
363
466
|
|
|
364
|
-
return
|
|
467
|
+
return {
|
|
468
|
+
highlightRange: replacement
|
|
469
|
+
? {
|
|
470
|
+
from: replacement.start,
|
|
471
|
+
to: replacement.start + replacement.insertText.length,
|
|
472
|
+
}
|
|
473
|
+
: null,
|
|
474
|
+
ok: true,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
applyExternalDeletion() {
|
|
479
|
+
this.markDeleted();
|
|
480
|
+
return this.destroy();
|
|
365
481
|
}
|
|
366
482
|
|
|
367
483
|
rename(nextName) {
|
|
@@ -403,6 +519,9 @@ export class CollaborationRoom {
|
|
|
403
519
|
}
|
|
404
520
|
|
|
405
521
|
sendInitialSync(ws) {
|
|
522
|
+
this.debugMetrics.initialSyncCount += 1;
|
|
523
|
+
this.debugMetrics.lastInitialSyncAt = Date.now();
|
|
524
|
+
console.info(`[perf][room:${this.name}] send-initial-sync count=${this.debugMetrics.initialSyncCount}`);
|
|
406
525
|
return sendMessage.call(this, ws, this.getInitialSyncMessage(), this);
|
|
407
526
|
}
|
|
408
527
|
|
|
@@ -537,6 +656,10 @@ export class CollaborationRoom {
|
|
|
537
656
|
}
|
|
538
657
|
|
|
539
658
|
getPersistedContent() {
|
|
659
|
+
if (isWorkspaceRoom(this.name)) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
|
|
540
663
|
if (!isExcalidrawRoom(this.name)) {
|
|
541
664
|
return this.doc.getText('codemirror').toString();
|
|
542
665
|
}
|
|
@@ -600,4 +723,93 @@ export class CollaborationRoom {
|
|
|
600
723
|
console.error(`[room:${this.name}] Failed to handle message:`, error.message);
|
|
601
724
|
}
|
|
602
725
|
}
|
|
726
|
+
|
|
727
|
+
replaceWorkspaceEntries(entries = new Map(), {
|
|
728
|
+
generatedAt = Date.now(),
|
|
729
|
+
} = {}) {
|
|
730
|
+
if (!isWorkspaceRoom(this.name)) {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const normalizedEntries = entries instanceof Map ? entries : new Map(entries);
|
|
735
|
+
const entriesMap = this.doc.getMap('entries');
|
|
736
|
+
const metaMap = this.doc.getMap('meta');
|
|
737
|
+
|
|
738
|
+
this.doc.transact(() => {
|
|
739
|
+
Array.from(entriesMap.keys()).forEach((key) => {
|
|
740
|
+
if (!normalizedEntries.has(key)) {
|
|
741
|
+
entriesMap.delete(key);
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
normalizedEntries.forEach((entry, pathValue) => {
|
|
746
|
+
entriesMap.set(pathValue, entry);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
metaMap.set('lastSnapshotAt', generatedAt);
|
|
750
|
+
metaMap.set('revision', Number(metaMap.get('revision') || 0) + 1);
|
|
751
|
+
}, 'workspace-room-entries');
|
|
752
|
+
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
applyWorkspaceEntryPatch({
|
|
757
|
+
deletes = [],
|
|
758
|
+
upserts = new Map(),
|
|
759
|
+
} = {}, {
|
|
760
|
+
generatedAt = Date.now(),
|
|
761
|
+
} = {}) {
|
|
762
|
+
if (!isWorkspaceRoom(this.name)) {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const normalizedUpserts = upserts instanceof Map ? upserts : new Map(upserts);
|
|
767
|
+
const normalizedDeletes = Array.from(new Set((deletes ?? []).filter(Boolean)));
|
|
768
|
+
if (normalizedUpserts.size === 0 && normalizedDeletes.length === 0) {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const entriesMap = this.doc.getMap('entries');
|
|
773
|
+
const metaMap = this.doc.getMap('meta');
|
|
774
|
+
|
|
775
|
+
this.doc.transact(() => {
|
|
776
|
+
normalizedDeletes.forEach((pathValue) => {
|
|
777
|
+
entriesMap.delete(pathValue);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
normalizedUpserts.forEach((entry, pathValue) => {
|
|
781
|
+
entriesMap.set(pathValue, entry);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
metaMap.set('lastSnapshotAt', generatedAt);
|
|
785
|
+
metaMap.set('revision', Number(metaMap.get('revision') || 0) + 1);
|
|
786
|
+
}, 'workspace-room-entry-patch');
|
|
787
|
+
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
publishWorkspaceEvent(event) {
|
|
792
|
+
if (!isWorkspaceRoom(this.name)) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const normalizedEvent = normalizeWorkspaceEvent(event);
|
|
797
|
+
if (!normalizedEvent) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const events = this.doc.getArray('events');
|
|
802
|
+
const metaMap = this.doc.getMap('meta');
|
|
803
|
+
this.doc.transact(() => {
|
|
804
|
+
events.push([normalizedEvent]);
|
|
805
|
+
const overflow = events.length - WORKSPACE_EVENT_MAX_MESSAGES;
|
|
806
|
+
if (overflow > 0) {
|
|
807
|
+
events.delete(0, overflow);
|
|
808
|
+
}
|
|
809
|
+
metaMap.set('lastEventAt', normalizedEvent.createdAt);
|
|
810
|
+
metaMap.set('revision', Number(metaMap.get('revision') || 0) + 1);
|
|
811
|
+
}, 'workspace-room-event');
|
|
812
|
+
|
|
813
|
+
return normalizedEvent;
|
|
814
|
+
}
|
|
603
815
|
}
|
|
@@ -57,27 +57,67 @@ export class RoomRegistry {
|
|
|
57
57
|
this.rooms.clear();
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
getRooms() {
|
|
61
|
+
return Array.from(this.rooms.entries());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async reloadAllFromDisk() {
|
|
65
|
+
await Promise.allSettled(
|
|
66
|
+
Array.from(this.rooms.values(), (room) => room.reloadFromDisk?.()),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
async reconcileWorkspaceChange(workspaceChange = {}) {
|
|
61
71
|
const deletedPaths = new Set(workspaceChange.deletedPaths ?? []);
|
|
62
72
|
const renamedPaths = Array.isArray(workspaceChange.renamedPaths) ? workspaceChange.renamedPaths : [];
|
|
63
|
-
const
|
|
73
|
+
const pendingDeletes = [];
|
|
74
|
+
const highlightRanges = [];
|
|
75
|
+
const reloadRequiredPaths = [];
|
|
64
76
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
77
|
+
renamedPaths.forEach((entry) => {
|
|
78
|
+
if (!entry?.oldPath || !entry?.newPath) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
71
81
|
|
|
82
|
+
if (this.rename(entry.oldPath, entry.newPath)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const room = this.rooms.get(entry.oldPath);
|
|
87
|
+
if (room) {
|
|
88
|
+
pendingDeletes.push([entry.oldPath, room]);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
deletedPaths.forEach((pathValue) => {
|
|
93
|
+
if (!pathValue) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const room = this.rooms.get(pathValue);
|
|
98
|
+
if (room) {
|
|
99
|
+
pendingDeletes.push([pathValue, room]);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await Promise.allSettled(
|
|
104
|
+
pendingDeletes.map(async ([pathValue, room]) => {
|
|
72
105
|
room.markDeleted?.();
|
|
73
|
-
|
|
106
|
+
if (typeof room.applyExternalDeletion === 'function') {
|
|
107
|
+
await room.applyExternalDeletion();
|
|
108
|
+
} else {
|
|
109
|
+
await room.destroy?.();
|
|
110
|
+
}
|
|
74
111
|
if (this.rooms.get(pathValue) === room) {
|
|
75
112
|
this.rooms.delete(pathValue);
|
|
76
113
|
}
|
|
77
114
|
}),
|
|
78
115
|
);
|
|
79
116
|
|
|
80
|
-
const blockedPaths = new Set([
|
|
117
|
+
const blockedPaths = new Set([
|
|
118
|
+
...deletedPaths,
|
|
119
|
+
...renamedPaths.flatMap((entry) => [entry?.oldPath, entry?.newPath]),
|
|
120
|
+
]);
|
|
81
121
|
await Promise.allSettled(
|
|
82
122
|
Array.from(new Set(workspaceChange.changedPaths ?? []))
|
|
83
123
|
.filter((pathValue) => pathValue && !blockedPaths.has(pathValue))
|
|
@@ -87,8 +127,22 @@ export class RoomRegistry {
|
|
|
87
127
|
return;
|
|
88
128
|
}
|
|
89
129
|
|
|
90
|
-
await room.reloadFromDisk?.();
|
|
130
|
+
const result = await room.reloadFromDisk?.();
|
|
131
|
+
if (result && result.ok === false && result.reason === 'invalid-excalidraw') {
|
|
132
|
+
reloadRequiredPaths.push(pathValue);
|
|
133
|
+
} else if (result?.highlightRange) {
|
|
134
|
+
highlightRanges.push({
|
|
135
|
+
from: result.highlightRange.from,
|
|
136
|
+
path: pathValue,
|
|
137
|
+
to: result.highlightRange.to,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
91
140
|
}),
|
|
92
141
|
);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
highlightRanges,
|
|
145
|
+
reloadRequiredPaths,
|
|
146
|
+
};
|
|
93
147
|
}
|
|
94
148
|
}
|
package/src/server/index.js
CHANGED
|
@@ -60,7 +60,9 @@ server.listen().then(({ host, port, wsPath }) => {
|
|
|
60
60
|
if (server.config.auth.strategy === 'password') {
|
|
61
61
|
console.log(` auth: password (${server.config.auth.password})`);
|
|
62
62
|
} else if (server.config.auth.strategy === 'oidc') {
|
|
63
|
-
console.log(
|
|
63
|
+
console.log(` auth: oidc (${server.config.auth.oidc.provider})`);
|
|
64
|
+
console.log(` public: ${server.config.auth.oidc.publicBaseUrl}`);
|
|
65
|
+
console.log(` callback: ${server.config.auth.oidc.callbackUrl}`);
|
|
64
66
|
} else {
|
|
65
67
|
console.log(' auth: none');
|
|
66
68
|
}
|
|
@@ -31,13 +31,14 @@ export class GitCommandRunner {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
async execGit(args) {
|
|
34
|
+
async execGit(args, { env = null } = {}) {
|
|
35
35
|
const result = await this.execFileImpl('git', ['-c', 'core.quotepath=false', ...args], {
|
|
36
36
|
cwd: this.vaultDir,
|
|
37
37
|
encoding: 'utf8',
|
|
38
38
|
env: {
|
|
39
39
|
...process.env,
|
|
40
40
|
...(this.commandEnv ?? {}),
|
|
41
|
+
...(env ?? {}),
|
|
41
42
|
},
|
|
42
43
|
maxBuffer: 5 * 1024 * 1024,
|
|
43
44
|
timeout: 10_000,
|
|
@@ -2,12 +2,29 @@ import { createGitRequestError } from './errors.js';
|
|
|
2
2
|
import { normalizeRelativeGitPath } from './path-utils.js';
|
|
3
3
|
import { GitCommandRunner } from './command-runner.js';
|
|
4
4
|
import { GitDiffService } from './diff-service.js';
|
|
5
|
+
import { GitHistoryService } from './history-service.js';
|
|
5
6
|
import { parseNameStatusOutput } from './parsers.js';
|
|
6
7
|
import { createEmptyWorkspaceChange, createWorkspaceChange } from './responses.js';
|
|
7
8
|
import { GitStatusService } from './status-service.js';
|
|
8
9
|
import { GitUntrackedFileService } from './untracked-files.js';
|
|
9
10
|
import { PullBackupStore } from '../persistence/pull-backup-store.js';
|
|
10
11
|
|
|
12
|
+
function buildAuthorEnv(author = null) {
|
|
13
|
+
const name = String(author?.name ?? '').trim();
|
|
14
|
+
const email = String(author?.email ?? '').trim();
|
|
15
|
+
|
|
16
|
+
if (!name || !email) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
GIT_AUTHOR_EMAIL: email,
|
|
22
|
+
GIT_AUTHOR_NAME: name,
|
|
23
|
+
GIT_COMMITTER_EMAIL: email,
|
|
24
|
+
GIT_COMMITTER_NAME: name,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
export class GitService {
|
|
12
29
|
constructor({
|
|
13
30
|
commandEnv = null,
|
|
@@ -37,6 +54,11 @@ export class GitService {
|
|
|
37
54
|
statusService: this.statusService,
|
|
38
55
|
untrackedFileService: this.untrackedFileService,
|
|
39
56
|
});
|
|
57
|
+
this.historyService = new GitHistoryService({
|
|
58
|
+
commandRunner: this.commandRunner,
|
|
59
|
+
maxInitialPatchBytes,
|
|
60
|
+
maxInitialPatchLines,
|
|
61
|
+
});
|
|
40
62
|
this.pullBackupStore = new PullBackupStore({ vaultDir });
|
|
41
63
|
}
|
|
42
64
|
|
|
@@ -51,6 +73,7 @@ export class GitService {
|
|
|
51
73
|
invalidateStatusCache() {
|
|
52
74
|
this.statusService.invalidate();
|
|
53
75
|
this.diffService.invalidate();
|
|
76
|
+
this.historyService.invalidate();
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
async getStatus(options = {}) {
|
|
@@ -87,7 +110,7 @@ export class GitService {
|
|
|
87
110
|
};
|
|
88
111
|
}
|
|
89
112
|
|
|
90
|
-
async commitStaged({ message } = {}) {
|
|
113
|
+
async commitStaged({ author = null, message } = {}) {
|
|
91
114
|
const normalizedMessage = String(message ?? '').trim();
|
|
92
115
|
if (!normalizedMessage) {
|
|
93
116
|
throw createGitRequestError(400, 'Missing commit message');
|
|
@@ -98,7 +121,9 @@ export class GitService {
|
|
|
98
121
|
throw createGitRequestError(409, 'No staged changes to commit');
|
|
99
122
|
}
|
|
100
123
|
|
|
101
|
-
await this.commandRunner.execGit(['commit', '-m', normalizedMessage]
|
|
124
|
+
await this.commandRunner.execGit(['commit', '-m', normalizedMessage], {
|
|
125
|
+
env: buildAuthorEnv(author),
|
|
126
|
+
});
|
|
102
127
|
const hash = (await this.commandRunner.execGit(['rev-parse', 'HEAD'])).trim();
|
|
103
128
|
const shortHash = (await this.commandRunner.execGit(['rev-parse', '--short', 'HEAD'])).trim();
|
|
104
129
|
this.invalidateStatusCache();
|
|
@@ -233,6 +258,27 @@ export class GitService {
|
|
|
233
258
|
});
|
|
234
259
|
}
|
|
235
260
|
|
|
261
|
+
async getHistory({ limit = 30, offset = 0 } = {}) {
|
|
262
|
+
return this.historyService.listHistory({ limit, offset });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async getFileHistory({ limit = 30, offset = 0, path } = {}) {
|
|
266
|
+
return this.historyService.listFileHistory({ limit, offset, path });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async getCommit({ allowLargePatch = false, hash, metaOnly = false, path = null } = {}) {
|
|
270
|
+
return this.historyService.getCommit({
|
|
271
|
+
allowLargePatch,
|
|
272
|
+
hash,
|
|
273
|
+
metaOnly,
|
|
274
|
+
path,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async getFileSnapshot({ hash, path } = {}) {
|
|
279
|
+
return this.historyService.getFileSnapshot({ hash, path });
|
|
280
|
+
}
|
|
281
|
+
|
|
236
282
|
async getHeadRef() {
|
|
237
283
|
try {
|
|
238
284
|
const output = await this.commandRunner.execGit(['rev-parse', 'HEAD']);
|