collabmd 0.1.20 → 0.1.21
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 +4 -2
- package/package.json +1 -1
- package/public/assets/css/style.css +1 -1
- package/public/assets/js/chunks/chunk-GFDM7YCF.js +1 -0
- package/public/assets/js/chunks/editor-session-EAUBJCHH.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-TCIH6GJL.js} +1 -1
- package/public/assets/js/excalidraw-editor.js +1 -1
- package/public/assets/js/main.js +68 -68
- package/public/assets/js/preview-render-worker.js +15 -15
- package/src/client/application/app-shell/git-feature.js +57 -18
- package/src/client/application/app-shell/ui-feature.js +4 -0
- package/src/client/bootstrap/collabmd-app-shell.js +12 -0
- package/src/client/domain/vault-utils.js +2 -2
- package/src/client/excalidraw-editor.js +2 -1
- package/src/client/infrastructure/editor-session.js +4 -0
- package/src/client/infrastructure/editor-view-adapter.js +181 -1
- package/src/client/infrastructure/workspace-sync-client.js +324 -0
- package/src/client/presentation/comment-ui-controller.js +192 -5
- package/src/client/presentation/file-explorer-controller.js +15 -3
- package/src/client/presentation/file-explorer-view.js +152 -11
- package/src/client/styles/style.css +46 -2
- 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/create-app-server.js +41 -10
- package/src/server/domain/backlink-index.js +94 -1
- package/src/server/domain/collaboration/collaboration-room.js +191 -22
- package/src/server/domain/collaboration/room-registry.js +64 -10
- package/src/server/infrastructure/git/responses.js +12 -19
- package/src/server/infrastructure/http/create-git-api-command-handler.js +53 -43
- package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
- package/src/server/infrastructure/http/create-request-handler.js +7 -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,6 +88,41 @@ 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,
|
|
@@ -332,36 +369,75 @@ export class CollaborationRoom {
|
|
|
332
369
|
return false;
|
|
333
370
|
}
|
|
334
371
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
372
|
+
return this.applyExternalContent(content, {
|
|
373
|
+
commentThreads,
|
|
374
|
+
replaceCommentThreads: true,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async applyExternalContent(content, {
|
|
379
|
+
commentThreads = [],
|
|
380
|
+
replaceCommentThreads = false,
|
|
381
|
+
} = {}) {
|
|
382
|
+
if (this.deleted || this.destroyed) {
|
|
383
|
+
return { ok: false, reason: 'room-unavailable' };
|
|
342
384
|
}
|
|
343
385
|
|
|
344
|
-
this.doc.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
386
|
+
const comments = this.doc.getArray('comments');
|
|
387
|
+
if (isExcalidrawRoom(this.name)) {
|
|
388
|
+
const parsedExcalidrawScene = tryParseExcalidrawSceneJson(content);
|
|
389
|
+
if (!parsedExcalidrawScene) {
|
|
390
|
+
return { ok: false, reason: 'invalid-excalidraw' };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
this.doc.transact(() => {
|
|
349
394
|
replaceExcalidrawRoomScene(this.doc, parsedExcalidrawScene);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
ytext.insert(0, content);
|
|
395
|
+
if (replaceCommentThreads) {
|
|
396
|
+
if (comments.length > 0) {
|
|
397
|
+
comments.delete(0, comments.length);
|
|
398
|
+
}
|
|
399
|
+
populateCommentThreads(comments, commentThreads);
|
|
356
400
|
}
|
|
401
|
+
}, 'workspace-reconcile');
|
|
402
|
+
|
|
403
|
+
return { ok: true, highlightRange: null };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const ytext = this.doc.getText('codemirror');
|
|
407
|
+
const replacement = computeTextReplacement(ytext.toString(), content);
|
|
408
|
+
if (!replacement && !replaceCommentThreads) {
|
|
409
|
+
return { highlightRange: null, ok: true, skipped: true };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
this.doc.transact(() => {
|
|
413
|
+
if (replacement?.deleteCount) {
|
|
414
|
+
ytext.delete(replacement.start, replacement.deleteCount);
|
|
357
415
|
}
|
|
358
|
-
if (
|
|
359
|
-
|
|
416
|
+
if (replacement?.insertText) {
|
|
417
|
+
ytext.insert(replacement.start, replacement.insertText);
|
|
418
|
+
}
|
|
419
|
+
if (replaceCommentThreads) {
|
|
420
|
+
if (comments.length > 0) {
|
|
421
|
+
comments.delete(0, comments.length);
|
|
422
|
+
}
|
|
423
|
+
populateCommentThreads(comments, commentThreads);
|
|
360
424
|
}
|
|
361
|
-
populateCommentThreads(comments, commentThreads);
|
|
362
425
|
}, 'workspace-reconcile');
|
|
363
426
|
|
|
364
|
-
return
|
|
427
|
+
return {
|
|
428
|
+
highlightRange: replacement
|
|
429
|
+
? {
|
|
430
|
+
from: replacement.start,
|
|
431
|
+
to: replacement.start + replacement.insertText.length,
|
|
432
|
+
}
|
|
433
|
+
: null,
|
|
434
|
+
ok: true,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
applyExternalDeletion() {
|
|
439
|
+
this.markDeleted();
|
|
440
|
+
return this.destroy();
|
|
365
441
|
}
|
|
366
442
|
|
|
367
443
|
rename(nextName) {
|
|
@@ -537,6 +613,10 @@ export class CollaborationRoom {
|
|
|
537
613
|
}
|
|
538
614
|
|
|
539
615
|
getPersistedContent() {
|
|
616
|
+
if (isWorkspaceRoom(this.name)) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
|
|
540
620
|
if (!isExcalidrawRoom(this.name)) {
|
|
541
621
|
return this.doc.getText('codemirror').toString();
|
|
542
622
|
}
|
|
@@ -600,4 +680,93 @@ export class CollaborationRoom {
|
|
|
600
680
|
console.error(`[room:${this.name}] Failed to handle message:`, error.message);
|
|
601
681
|
}
|
|
602
682
|
}
|
|
683
|
+
|
|
684
|
+
replaceWorkspaceEntries(entries = new Map(), {
|
|
685
|
+
generatedAt = Date.now(),
|
|
686
|
+
} = {}) {
|
|
687
|
+
if (!isWorkspaceRoom(this.name)) {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const normalizedEntries = entries instanceof Map ? entries : new Map(entries);
|
|
692
|
+
const entriesMap = this.doc.getMap('entries');
|
|
693
|
+
const metaMap = this.doc.getMap('meta');
|
|
694
|
+
|
|
695
|
+
this.doc.transact(() => {
|
|
696
|
+
Array.from(entriesMap.keys()).forEach((key) => {
|
|
697
|
+
if (!normalizedEntries.has(key)) {
|
|
698
|
+
entriesMap.delete(key);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
normalizedEntries.forEach((entry, pathValue) => {
|
|
703
|
+
entriesMap.set(pathValue, entry);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
metaMap.set('lastSnapshotAt', generatedAt);
|
|
707
|
+
metaMap.set('revision', Number(metaMap.get('revision') || 0) + 1);
|
|
708
|
+
}, 'workspace-room-entries');
|
|
709
|
+
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
applyWorkspaceEntryPatch({
|
|
714
|
+
deletes = [],
|
|
715
|
+
upserts = new Map(),
|
|
716
|
+
} = {}, {
|
|
717
|
+
generatedAt = Date.now(),
|
|
718
|
+
} = {}) {
|
|
719
|
+
if (!isWorkspaceRoom(this.name)) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const normalizedUpserts = upserts instanceof Map ? upserts : new Map(upserts);
|
|
724
|
+
const normalizedDeletes = Array.from(new Set((deletes ?? []).filter(Boolean)));
|
|
725
|
+
if (normalizedUpserts.size === 0 && normalizedDeletes.length === 0) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const entriesMap = this.doc.getMap('entries');
|
|
730
|
+
const metaMap = this.doc.getMap('meta');
|
|
731
|
+
|
|
732
|
+
this.doc.transact(() => {
|
|
733
|
+
normalizedDeletes.forEach((pathValue) => {
|
|
734
|
+
entriesMap.delete(pathValue);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
normalizedUpserts.forEach((entry, pathValue) => {
|
|
738
|
+
entriesMap.set(pathValue, entry);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
metaMap.set('lastSnapshotAt', generatedAt);
|
|
742
|
+
metaMap.set('revision', Number(metaMap.get('revision') || 0) + 1);
|
|
743
|
+
}, 'workspace-room-entry-patch');
|
|
744
|
+
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
publishWorkspaceEvent(event) {
|
|
749
|
+
if (!isWorkspaceRoom(this.name)) {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const normalizedEvent = normalizeWorkspaceEvent(event);
|
|
754
|
+
if (!normalizedEvent) {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const events = this.doc.getArray('events');
|
|
759
|
+
const metaMap = this.doc.getMap('meta');
|
|
760
|
+
this.doc.transact(() => {
|
|
761
|
+
events.push([normalizedEvent]);
|
|
762
|
+
const overflow = events.length - WORKSPACE_EVENT_MAX_MESSAGES;
|
|
763
|
+
if (overflow > 0) {
|
|
764
|
+
events.delete(0, overflow);
|
|
765
|
+
}
|
|
766
|
+
metaMap.set('lastEventAt', normalizedEvent.createdAt);
|
|
767
|
+
metaMap.set('revision', Number(metaMap.get('revision') || 0) + 1);
|
|
768
|
+
}, 'workspace-room-event');
|
|
769
|
+
|
|
770
|
+
return normalizedEvent;
|
|
771
|
+
}
|
|
603
772
|
}
|
|
@@ -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
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEmptyWorkspaceChange as createSharedEmptyWorkspaceChange,
|
|
3
|
+
createWorkspaceChange as createSharedWorkspaceChange,
|
|
4
|
+
} from '../../../domain/workspace-change.js';
|
|
5
|
+
|
|
1
6
|
const STATUS_MAP = {
|
|
2
7
|
A: { code: 'A', label: 'added', status: 'added' },
|
|
3
8
|
C: { code: 'C', label: 'copied', status: 'copied' },
|
|
@@ -43,12 +48,7 @@ export function createEmptyBranchStatus() {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
export function createEmptyWorkspaceChange() {
|
|
46
|
-
return
|
|
47
|
-
changedPaths: [],
|
|
48
|
-
deletedPaths: [],
|
|
49
|
-
refreshExplorer: true,
|
|
50
|
-
renamedPaths: [],
|
|
51
|
-
};
|
|
51
|
+
return createSharedEmptyWorkspaceChange();
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function createEmptyStatusResponse() {
|
|
@@ -92,17 +92,10 @@ export function createWorkspaceChange({
|
|
|
92
92
|
refreshExplorer = true,
|
|
93
93
|
renamedPaths = [],
|
|
94
94
|
} = {}) {
|
|
95
|
-
return {
|
|
96
|
-
changedPaths
|
|
97
|
-
deletedPaths
|
|
98
|
-
refreshExplorer
|
|
99
|
-
renamedPaths
|
|
100
|
-
|
|
101
|
-
.filter((entry) => entry?.oldPath && entry?.newPath && entry.oldPath !== entry.newPath)
|
|
102
|
-
.map((entry) => [`${entry.oldPath}:${entry.newPath}`, {
|
|
103
|
-
newPath: entry.newPath,
|
|
104
|
-
oldPath: entry.oldPath,
|
|
105
|
-
}]),
|
|
106
|
-
).values()),
|
|
107
|
-
};
|
|
95
|
+
return createSharedWorkspaceChange({
|
|
96
|
+
changedPaths,
|
|
97
|
+
deletedPaths,
|
|
98
|
+
refreshExplorer,
|
|
99
|
+
renamedPaths,
|
|
100
|
+
});
|
|
108
101
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getRequestErrorStatusCode } from './http-errors.js';
|
|
2
2
|
import { jsonResponse } from './http-response.js';
|
|
3
3
|
import { parseJsonBody } from './request-body.js';
|
|
4
|
-
import { createEmptyWorkspaceChange } from '
|
|
4
|
+
import { createEmptyWorkspaceChange, hasWorkspaceMutation } from '../../../domain/workspace-change.js';
|
|
5
5
|
|
|
6
6
|
async function parseRequiredBody(req, res, fieldName) {
|
|
7
7
|
const body = await parseJsonBody(req);
|
|
@@ -29,19 +29,16 @@ function handleGitError(req, res, error, logMessage, fallbackMessage) {
|
|
|
29
29
|
return true;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|| (workspaceChange.deletedPaths?.length ?? 0) > 0
|
|
36
|
-
|| (workspaceChange.renamedPaths?.length ?? 0) > 0,
|
|
37
|
-
);
|
|
32
|
+
function readRequestId(req) {
|
|
33
|
+
const value = String(req.headers['x-collabmd-request-id'] || '').trim();
|
|
34
|
+
return value ? value.slice(0, 120) : null;
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
async function applyWorkspaceMutationEffects({
|
|
41
|
-
|
|
38
|
+
action,
|
|
39
|
+
req,
|
|
42
40
|
responsePayload,
|
|
43
|
-
|
|
44
|
-
vaultFileStore,
|
|
41
|
+
workspaceMutationCoordinator,
|
|
45
42
|
}) {
|
|
46
43
|
const workspaceChange = responsePayload?.workspaceChange ?? createEmptyWorkspaceChange();
|
|
47
44
|
responsePayload.workspaceChange = workspaceChange;
|
|
@@ -50,18 +47,19 @@ async function applyWorkspaceMutationEffects({
|
|
|
50
47
|
return responsePayload;
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
await
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
await workspaceMutationCoordinator?.apply?.({
|
|
51
|
+
action,
|
|
52
|
+
origin: 'git',
|
|
53
|
+
requestId: readRequestId(req),
|
|
54
|
+
sourceRef: responsePayload?.sourceRef ?? null,
|
|
55
|
+
workspaceChange,
|
|
56
|
+
});
|
|
57
57
|
return responsePayload;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function createGitApiCommandHandler({
|
|
61
|
-
backlinkIndex = null,
|
|
62
61
|
gitService,
|
|
63
|
-
|
|
64
|
-
vaultFileStore = null,
|
|
62
|
+
workspaceMutationCoordinator = null,
|
|
65
63
|
}) {
|
|
66
64
|
return async function handleGitApiCommand(req, res, requestUrl) {
|
|
67
65
|
if (requestUrl.pathname === '/api/git/stage' && req.method === 'POST') {
|
|
@@ -72,10 +70,12 @@ export function createGitApiCommandHandler({
|
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
action: 'stage',
|
|
74
|
+
req,
|
|
75
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
76
|
+
() => gitService.stageFile(body.path),
|
|
77
|
+
),
|
|
78
|
+
workspaceMutationCoordinator,
|
|
79
79
|
}));
|
|
80
80
|
} catch (error) {
|
|
81
81
|
handleGitError(req, res, error, '[api] Failed to stage git file:', 'Failed to stage git file');
|
|
@@ -91,10 +91,12 @@ export function createGitApiCommandHandler({
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
action: 'unstage',
|
|
95
|
+
req,
|
|
96
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
97
|
+
() => gitService.unstageFile(body.path),
|
|
98
|
+
),
|
|
99
|
+
workspaceMutationCoordinator,
|
|
98
100
|
}));
|
|
99
101
|
} catch (error) {
|
|
100
102
|
handleGitError(req, res, error, '[api] Failed to unstage git file:', 'Failed to unstage git file');
|
|
@@ -110,12 +112,14 @@ export function createGitApiCommandHandler({
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
action: 'commit',
|
|
116
|
+
req,
|
|
117
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
118
|
+
() => gitService.commitStaged({
|
|
119
|
+
message: body.message,
|
|
120
|
+
}),
|
|
121
|
+
),
|
|
122
|
+
workspaceMutationCoordinator,
|
|
119
123
|
}));
|
|
120
124
|
} catch (error) {
|
|
121
125
|
handleGitError(req, res, error, '[api] Failed to commit staged changes:', 'Failed to commit staged changes');
|
|
@@ -126,10 +130,12 @@ export function createGitApiCommandHandler({
|
|
|
126
130
|
if (requestUrl.pathname === '/api/git/push' && req.method === 'POST') {
|
|
127
131
|
try {
|
|
128
132
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
action: 'push',
|
|
134
|
+
req,
|
|
135
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
136
|
+
() => gitService.pushBranch(),
|
|
137
|
+
),
|
|
138
|
+
workspaceMutationCoordinator,
|
|
133
139
|
}));
|
|
134
140
|
} catch (error) {
|
|
135
141
|
handleGitError(req, res, error, '[api] Failed to push git branch:', 'Failed to push git branch');
|
|
@@ -140,10 +146,12 @@ export function createGitApiCommandHandler({
|
|
|
140
146
|
if (requestUrl.pathname === '/api/git/pull' && req.method === 'POST') {
|
|
141
147
|
try {
|
|
142
148
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
action: 'pull',
|
|
150
|
+
req,
|
|
151
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
152
|
+
() => gitService.pullBranch(),
|
|
153
|
+
),
|
|
154
|
+
workspaceMutationCoordinator,
|
|
147
155
|
}));
|
|
148
156
|
} catch (error) {
|
|
149
157
|
handleGitError(req, res, error, '[api] Failed to pull git branch:', 'Failed to pull git branch');
|
|
@@ -159,10 +167,12 @@ export function createGitApiCommandHandler({
|
|
|
159
167
|
}
|
|
160
168
|
|
|
161
169
|
jsonResponse(req, res, 200, await applyWorkspaceMutationEffects({
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
action: 'reset-file',
|
|
171
|
+
req,
|
|
172
|
+
responsePayload: await workspaceMutationCoordinator.runManagedWorkspaceMutation(
|
|
173
|
+
() => gitService.resetFileToHead(body.path),
|
|
174
|
+
),
|
|
175
|
+
workspaceMutationCoordinator,
|
|
166
176
|
}));
|
|
167
177
|
} catch (error) {
|
|
168
178
|
handleGitError(req, res, error, '[api] Failed to reset git file:', 'Failed to reset git file');
|
|
@@ -7,6 +7,7 @@ export function createGitApiHandler({
|
|
|
7
7
|
gitService = null,
|
|
8
8
|
roomRegistry = null,
|
|
9
9
|
vaultFileStore = null,
|
|
10
|
+
workspaceMutationCoordinator = null,
|
|
10
11
|
}) {
|
|
11
12
|
const handleGitApiQuery = createGitApiQueryHandler({ gitService });
|
|
12
13
|
const handleGitApiCommand = createGitApiCommandHandler({
|
|
@@ -14,6 +15,7 @@ export function createGitApiHandler({
|
|
|
14
15
|
gitService,
|
|
15
16
|
roomRegistry,
|
|
16
17
|
vaultFileStore,
|
|
18
|
+
workspaceMutationCoordinator,
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
return async function handleGitApi(req, res, requestUrl) {
|
|
@@ -42,6 +42,8 @@ export function createRequestHandler(
|
|
|
42
42
|
roomRegistry = null,
|
|
43
43
|
plantUmlRenderer = null,
|
|
44
44
|
gitService = null,
|
|
45
|
+
workspaceMutationCoordinator = null,
|
|
46
|
+
fileSystemSyncService = null,
|
|
45
47
|
) {
|
|
46
48
|
const handleEsmProxy = createEsmProxyHandler();
|
|
47
49
|
const handleStaticRequest = createStaticHandler(config, authService);
|
|
@@ -51,12 +53,14 @@ export function createRequestHandler(
|
|
|
51
53
|
gitService,
|
|
52
54
|
roomRegistry,
|
|
53
55
|
vaultFileStore,
|
|
56
|
+
workspaceMutationCoordinator,
|
|
54
57
|
});
|
|
55
58
|
const handleVaultApi = createVaultApiHandler({
|
|
56
59
|
backlinkIndex,
|
|
57
60
|
plantUmlRenderer,
|
|
58
61
|
roomRegistry,
|
|
59
62
|
vaultFileStore,
|
|
63
|
+
workspaceMutationCoordinator,
|
|
60
64
|
});
|
|
61
65
|
|
|
62
66
|
return async function handleRequest(req, res) {
|
|
@@ -103,8 +107,11 @@ export function createRequestHandler(
|
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
if (config.nodeEnv === 'test' && requestUrl.pathname === '/api/test/reset-state' && req.method === 'POST') {
|
|
110
|
+
await fileSystemSyncService?.resetForExternalStateChange?.();
|
|
106
111
|
await roomRegistry?.reset?.();
|
|
107
112
|
await backlinkIndex?.build?.();
|
|
113
|
+
await workspaceMutationCoordinator?.initialize?.();
|
|
114
|
+
await fileSystemSyncService?.resetForExternalStateChange?.();
|
|
108
115
|
jsonResponse(req, res, 200, { ok: true });
|
|
109
116
|
return;
|
|
110
117
|
}
|