collabmd 0.1.21 → 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 +94 -7
- 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/editor-session-S5AT7BMV.js +22 -0
- package/public/assets/js/{excalidraw-editor-TCIH6GJL.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/index.html +104 -84
- package/src/client/application/app-shell/git-feature.js +247 -9
- package/src/client/application/app-shell/ui-feature.js +65 -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 +95 -0
- package/src/client/domain/runtime-paths.js +5 -0
- package/src/client/excalidraw-editor.js +50 -2
- 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 +47 -7
- package/src/client/infrastructure/editor-view-adapter.js +122 -58
- package/src/client/infrastructure/runtime-config.js +66 -0
- package/src/client/presentation/comment-ui-controller.js +82 -26
- package/src/client/presentation/comments-panel.js +2 -2
- package/src/client/presentation/excalidraw-embed-controller.js +60 -2
- 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 -4945
- 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 +8 -0
- package/src/server/domain/collaboration/collaboration-room.js +90 -47
- 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 +73 -0
- package/src/server/infrastructure/http/create-auth-api-handler.js +30 -2
- package/src/server/infrastructure/http/create-git-api-command-handler.js +2 -0
- package/src/server/infrastructure/http/create-git-api-handler.js +2 -0
- package/src/server/infrastructure/http/create-git-api-query-handler.js +61 -0
- package/src/server/infrastructure/http/create-request-handler.js +15 -0
- package/public/assets/js/chunks/editor-session-EAUBJCHH.js +0 -22
package/src/server/config/env.js
CHANGED
|
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'url';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
AUTH_STRATEGY_NONE,
|
|
6
|
+
AUTH_STRATEGY_OIDC,
|
|
6
7
|
AUTH_STRATEGY_PASSWORD,
|
|
7
8
|
SUPPORTED_AUTH_STRATEGIES,
|
|
8
9
|
createRandomAuthPassword,
|
|
@@ -54,6 +55,30 @@ function normalizeWsBasePath(basePath) {
|
|
|
54
55
|
return normalizeRoutePath(basePath, '/ws');
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
function normalizePublicBaseUrl(rawValue) {
|
|
59
|
+
const normalized = normalizeOptionalString(rawValue);
|
|
60
|
+
if (!normalized) {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let parsedUrl;
|
|
65
|
+
try {
|
|
66
|
+
parsedUrl = new URL(normalized);
|
|
67
|
+
} catch {
|
|
68
|
+
throw new Error('PUBLIC_BASE_URL must be an absolute URL.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (parsedUrl.protocol !== 'https:' && parsedUrl.hostname !== 'localhost' && parsedUrl.hostname !== '127.0.0.1') {
|
|
72
|
+
throw new Error('PUBLIC_BASE_URL must use https unless it points to localhost.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if ((parsedUrl.pathname && parsedUrl.pathname !== '/') || parsedUrl.search || parsedUrl.hash) {
|
|
76
|
+
throw new Error('PUBLIC_BASE_URL must not include a path, query string, or hash.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return parsedUrl.origin;
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
function normalizeAuthStrategy(rawStrategy) {
|
|
58
83
|
const normalized = String(rawStrategy ?? AUTH_STRATEGY_NONE).trim().toLowerCase();
|
|
59
84
|
if (!SUPPORTED_AUTH_STRATEGIES.has(normalized)) {
|
|
@@ -65,8 +90,42 @@ function normalizeAuthStrategy(rawStrategy) {
|
|
|
65
90
|
return normalized;
|
|
66
91
|
}
|
|
67
92
|
|
|
93
|
+
function normalizeCsvList(value) {
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
return value
|
|
96
|
+
.map((entry) => String(entry ?? '').trim())
|
|
97
|
+
.filter(Boolean);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return String(value ?? '')
|
|
101
|
+
.split(',')
|
|
102
|
+
.map((entry) => entry.trim())
|
|
103
|
+
.filter(Boolean);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeEmailAllowlist(value) {
|
|
107
|
+
return normalizeCsvList(value)
|
|
108
|
+
.map((entry) => entry.toLowerCase());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeDomainAllowlist(value) {
|
|
112
|
+
return normalizeCsvList(value)
|
|
113
|
+
.map((entry) => entry.replace(/^@+/, '').toLowerCase())
|
|
114
|
+
.filter(Boolean);
|
|
115
|
+
}
|
|
116
|
+
|
|
68
117
|
const projectRoot = resolve(fileURLToPath(new URL('../../../', import.meta.url)));
|
|
69
118
|
|
|
119
|
+
export function resolveConfiguredVaultDir(overrides = {}, env = process.env) {
|
|
120
|
+
return overrides.vaultDir
|
|
121
|
+
|| env.COLLABMD_VAULT_DIR
|
|
122
|
+
|| resolve(projectRoot, 'data/vault');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resolveCliVaultDir(positionals = [], env = process.env) {
|
|
126
|
+
return resolve(positionals[0] || env.COLLABMD_VAULT_DIR || '.');
|
|
127
|
+
}
|
|
128
|
+
|
|
70
129
|
function getDefaultHost(nodeEnv) {
|
|
71
130
|
return nodeEnv === 'production' ? '0.0.0.0' : '127.0.0.1';
|
|
72
131
|
}
|
|
@@ -134,11 +193,63 @@ function loadGitConfig(overrides = {}) {
|
|
|
134
193
|
};
|
|
135
194
|
}
|
|
136
195
|
|
|
196
|
+
function loadOidcConfig(overrides = {}, { basePath = '' } = {}) {
|
|
197
|
+
const clientId = normalizeOptionalString(
|
|
198
|
+
overrides.clientId
|
|
199
|
+
?? process.env.AUTH_OIDC_CLIENT_ID,
|
|
200
|
+
);
|
|
201
|
+
const clientSecret = normalizeOptionalString(
|
|
202
|
+
overrides.clientSecret
|
|
203
|
+
?? process.env.AUTH_OIDC_CLIENT_SECRET,
|
|
204
|
+
);
|
|
205
|
+
const publicBaseUrl = normalizePublicBaseUrl(
|
|
206
|
+
overrides.publicBaseUrl
|
|
207
|
+
?? process.env.PUBLIC_BASE_URL,
|
|
208
|
+
);
|
|
209
|
+
const issuer = normalizeOptionalString(
|
|
210
|
+
overrides.issuer
|
|
211
|
+
?? process.env.AUTH_OIDC_ISSUER_URL,
|
|
212
|
+
) || 'https://accounts.google.com';
|
|
213
|
+
const flowCookieName = normalizeOptionalString(
|
|
214
|
+
overrides.flowCookieName
|
|
215
|
+
?? process.env.AUTH_OIDC_FLOW_COOKIE_NAME,
|
|
216
|
+
) || 'collabmd_auth_flow';
|
|
217
|
+
const allowedEmails = normalizeEmailAllowlist(
|
|
218
|
+
overrides.allowedEmails
|
|
219
|
+
?? process.env.AUTH_OIDC_ALLOWED_EMAILS,
|
|
220
|
+
);
|
|
221
|
+
const allowedDomains = normalizeDomainAllowlist(
|
|
222
|
+
overrides.allowedDomains
|
|
223
|
+
?? process.env.AUTH_OIDC_ALLOWED_DOMAINS,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
if (!publicBaseUrl) {
|
|
227
|
+
throw new Error('OIDC auth requires PUBLIC_BASE_URL.');
|
|
228
|
+
}
|
|
229
|
+
if (!clientId) {
|
|
230
|
+
throw new Error('OIDC auth requires AUTH_OIDC_CLIENT_ID.');
|
|
231
|
+
}
|
|
232
|
+
if (!clientSecret) {
|
|
233
|
+
throw new Error('OIDC auth requires AUTH_OIDC_CLIENT_SECRET.');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
allowedDomains,
|
|
238
|
+
allowedEmails,
|
|
239
|
+
callbackUrl: `${publicBaseUrl}${basePath}/api/auth/oidc/callback`,
|
|
240
|
+
clientId,
|
|
241
|
+
clientSecret,
|
|
242
|
+
flowCookieName,
|
|
243
|
+
issuer,
|
|
244
|
+
provider: 'google',
|
|
245
|
+
publicBaseUrl,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
137
249
|
export function loadConfig(overrides = {}) {
|
|
138
250
|
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
139
|
-
const vaultDir = overrides
|
|
140
|
-
|
|
141
|
-
|| resolve(projectRoot, 'data/vault');
|
|
251
|
+
const vaultDir = resolveConfiguredVaultDir(overrides);
|
|
252
|
+
const basePath = normalizeAppBasePath(process.env.BASE_PATH || '');
|
|
142
253
|
const authOverrides = overrides.auth ?? {};
|
|
143
254
|
const authStrategy = normalizeAuthStrategy(
|
|
144
255
|
authOverrides.strategy
|
|
@@ -150,18 +261,22 @@ export function loadConfig(overrides = {}) {
|
|
|
150
261
|
const password = authStrategy === AUTH_STRATEGY_PASSWORD
|
|
151
262
|
? (authOverrides.password || process.env.AUTH_PASSWORD || createRandomAuthPassword())
|
|
152
263
|
: '';
|
|
264
|
+
const oidc = authStrategy === AUTH_STRATEGY_OIDC
|
|
265
|
+
? loadOidcConfig(authOverrides.oidc, { basePath })
|
|
266
|
+
: null;
|
|
153
267
|
const git = loadGitConfig(overrides.git);
|
|
154
268
|
|
|
155
269
|
return {
|
|
156
270
|
auth: {
|
|
157
271
|
generatedPassword: passwordWasGenerated ? password : '',
|
|
272
|
+
oidc,
|
|
158
273
|
password,
|
|
159
274
|
passwordWasGenerated,
|
|
160
275
|
sessionCookieName: authOverrides.sessionCookieName || process.env.AUTH_SESSION_COOKIE_NAME || 'collabmd_auth',
|
|
161
276
|
sessionSecret: authOverrides.sessionSecret || process.env.AUTH_SESSION_SECRET || createRandomSessionSecret(),
|
|
162
277
|
strategy: authStrategy,
|
|
163
278
|
},
|
|
164
|
-
basePath
|
|
279
|
+
basePath,
|
|
165
280
|
host: process.env.HOST || getDefaultHost(nodeEnv),
|
|
166
281
|
httpHeadersTimeoutMs: parsePositiveInt(process.env.HTTP_HEADERS_TIMEOUT_MS, 60_000),
|
|
167
282
|
httpKeepAliveTimeoutMs: parsePositiveInt(process.env.HTTP_KEEP_ALIVE_TIMEOUT_MS, 5_000),
|
|
@@ -174,8 +289,9 @@ export function loadConfig(overrides = {}) {
|
|
|
174
289
|
publicDir: resolve(projectRoot, 'public'),
|
|
175
290
|
vaultDir,
|
|
176
291
|
publicWsBaseUrl: process.env.PUBLIC_WS_BASE_URL || '',
|
|
292
|
+
testWsRoomHydrateDelayMs: parsePositiveInt(process.env.TEST_WS_ROOM_HYDRATE_DELAY_MS, 0),
|
|
177
293
|
wsHeartbeatIntervalMs: parsePositiveInt(process.env.WS_HEARTBEAT_INTERVAL_MS, 30_000),
|
|
178
|
-
wsRoomIdleGraceMs: parsePositiveInt(process.env.WS_ROOM_IDLE_GRACE_MS,
|
|
294
|
+
wsRoomIdleGraceMs: parsePositiveInt(process.env.WS_ROOM_IDLE_GRACE_MS, 60_000),
|
|
179
295
|
wsBasePath: normalizeWsBasePath(process.env.WS_BASE_PATH || '/ws'),
|
|
180
296
|
wsMaxBufferedAmountBytes: parsePositiveInt(
|
|
181
297
|
process.env.WS_MAX_BUFFERED_AMOUNT_BYTES,
|
|
@@ -48,6 +48,9 @@ export function createAppServer(config = loadConfig()) {
|
|
|
48
48
|
enabled: config.gitEnabled,
|
|
49
49
|
vaultDir: config.vaultDir,
|
|
50
50
|
});
|
|
51
|
+
const testControls = {
|
|
52
|
+
wsRoomHydrateDelayMs: Math.max(0, Number(config.testWsRoomHydrateDelayMs || 0)),
|
|
53
|
+
};
|
|
51
54
|
let workspaceMutationCoordinator = null;
|
|
52
55
|
const roomRegistry = new RoomRegistry({
|
|
53
56
|
createRoom: ({ name, onEmpty }) => {
|
|
@@ -57,6 +60,7 @@ export function createAppServer(config = loadConfig()) {
|
|
|
57
60
|
name,
|
|
58
61
|
vaultFileStore: name === '__lobby__' || name === WORKSPACE_ROOM_NAME ? null : vaultFileStore,
|
|
59
62
|
}),
|
|
63
|
+
getHydrateDelayMs: () => testControls.wsRoomHydrateDelayMs,
|
|
60
64
|
idleGraceMs: config.wsRoomIdleGraceMs,
|
|
61
65
|
maxBufferedAmountBytes: config.wsMaxBufferedAmountBytes,
|
|
62
66
|
name,
|
|
@@ -90,6 +94,7 @@ export function createAppServer(config = loadConfig()) {
|
|
|
90
94
|
roomRegistry,
|
|
91
95
|
plantUmlRenderer,
|
|
92
96
|
gitService,
|
|
97
|
+
testControls,
|
|
93
98
|
workspaceMutationCoordinator,
|
|
94
99
|
fileSystemSyncService,
|
|
95
100
|
);
|
|
@@ -168,6 +173,9 @@ export function createAppServer(config = loadConfig()) {
|
|
|
168
173
|
authService,
|
|
169
174
|
fileSystemSyncService,
|
|
170
175
|
gitService,
|
|
176
|
+
setTestHydrateDelayMs(delayMs = 0) {
|
|
177
|
+
testControls.wsRoomHydrateDelayMs = Math.max(0, Number(delayMs) || 0);
|
|
178
|
+
},
|
|
171
179
|
vaultFileStore,
|
|
172
180
|
get vaultFileCount() { return vaultFileCount; },
|
|
173
181
|
};
|
|
@@ -126,6 +126,7 @@ function computeTextReplacement(currentContent, nextContent) {
|
|
|
126
126
|
export class CollaborationRoom {
|
|
127
127
|
constructor({
|
|
128
128
|
documentStore = null,
|
|
129
|
+
getHydrateDelayMs = null,
|
|
129
130
|
name,
|
|
130
131
|
idleGraceMs = 0,
|
|
131
132
|
maxBufferedAmountBytes,
|
|
@@ -141,6 +142,7 @@ export class CollaborationRoom {
|
|
|
141
142
|
name,
|
|
142
143
|
vaultFileStore,
|
|
143
144
|
});
|
|
145
|
+
this.getHydrateDelayMs = getHydrateDelayMs;
|
|
144
146
|
this.onEmpty = onEmpty;
|
|
145
147
|
this.doc = new Y.Doc({ gc: true });
|
|
146
148
|
this.awareness = new awarenessProtocol.Awareness(this.doc);
|
|
@@ -152,6 +154,12 @@ export class CollaborationRoom {
|
|
|
152
154
|
this.destroyed = false;
|
|
153
155
|
this.cachedInitialSyncMessage = null;
|
|
154
156
|
this.activePersistPromise = null;
|
|
157
|
+
this.debugMetrics = {
|
|
158
|
+
hydrateCount: 0,
|
|
159
|
+
initialSyncCount: 0,
|
|
160
|
+
lastHydrate: null,
|
|
161
|
+
lastInitialSyncAt: 0,
|
|
162
|
+
};
|
|
155
163
|
this.persistence = new RoomPersistenceController({
|
|
156
164
|
idleGraceMs: this.idleGraceMs,
|
|
157
165
|
onDestroy: () => {
|
|
@@ -173,64 +181,96 @@ export class CollaborationRoom {
|
|
|
173
181
|
|
|
174
182
|
if (!this.hydratePromise) {
|
|
175
183
|
this.hydratePromise = (async () => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
}
|
|
187
195
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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');
|
|
191
215
|
}
|
|
192
|
-
} else {
|
|
193
|
-
Y.applyUpdate(this.doc, snapshot, 'hydrate');
|
|
194
|
-
}
|
|
195
216
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
}
|
|
203
227
|
}
|
|
204
|
-
}
|
|
205
228
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
|
|
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
|
+
}
|
|
218
246
|
} else if (content !== null) {
|
|
219
247
|
ytext.insert(0, content);
|
|
220
248
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
populateCommentThreads(comments, commentThreads);
|
|
225
|
-
}, 'hydrate');
|
|
249
|
+
populateCommentThreads(comments, commentThreads);
|
|
250
|
+
}, 'hydrate');
|
|
226
251
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
}
|
|
230
256
|
}
|
|
231
|
-
}
|
|
232
257
|
|
|
233
|
-
|
|
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
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
234
274
|
})().catch((error) => {
|
|
235
275
|
this.hydratePromise = null;
|
|
236
276
|
console.error(`[room:${this.name}] Failed to hydrate from disk: ${error.message}`);
|
|
@@ -479,6 +519,9 @@ export class CollaborationRoom {
|
|
|
479
519
|
}
|
|
480
520
|
|
|
481
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}`);
|
|
482
525
|
return sendMessage.call(this, ws, this.getInitialSyncMessage(), this);
|
|
483
526
|
}
|
|
484
527
|
|
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']);
|