collabmd 0.1.33 → 0.1.34

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.
@@ -371,10 +371,23 @@ export class BacklinkIndex {
371
371
  previousState = null,
372
372
  nextState = null,
373
373
  } = {}) {
374
- let refreshIndex = false;
375
374
  const previousEntries = previousState?.entries ?? new Map();
376
375
  const nextEntries = nextState?.entries ?? new Map();
377
376
  const changedPaths = Array.from(new Set(workspaceChange.changedPaths ?? []));
377
+ const { impactedSources, renameMap } = this._computeWorkspaceChangeMeta(workspaceChange, changedPaths, previousEntries, nextEntries);
378
+
379
+ let refreshIndex = this._applyDeletedPaths(workspaceChange.deletedPaths);
380
+ refreshIndex = this._applyRenamedPaths(workspaceChange.renamedPaths, refreshIndex);
381
+ refreshIndex = await this._applyChangedPaths(changedPaths, previousEntries, nextEntries, refreshIndex);
382
+
383
+ if (refreshIndex) {
384
+ const sourcesToRefresh = this._collectRefreshSources(impactedSources, changedPaths, workspaceChange.renamedPaths, nextEntries);
385
+ this._refreshWikiTargetIndex();
386
+ await this._refreshImpactedSources(sourcesToRefresh, { nextEntries, renameMap });
387
+ }
388
+ }
389
+
390
+ _computeWorkspaceChangeMeta(workspaceChange, changedPaths, previousEntries, nextEntries) {
378
391
  const createdPaths = changedPaths.filter((pathValue) => (
379
392
  nextEntries.has(pathValue) && !previousEntries.has(pathValue)
380
393
  ));
@@ -393,18 +406,28 @@ export class BacklinkIndex {
393
406
  .filter((entry) => entry?.oldPath && entry?.newPath)
394
407
  .map((entry) => [entry.oldPath, entry.newPath]),
395
408
  );
409
+ return { impactedSources, renameMap };
410
+ }
396
411
 
397
- for (const pathValue of workspaceChange.deletedPaths ?? []) {
412
+ _applyDeletedPaths(deletedPaths) {
413
+ let refreshIndex = false;
414
+ for (const pathValue of deletedPaths ?? []) {
398
415
  refreshIndex = this.onFileDeleted(pathValue, { refreshIndex: false }) || refreshIndex;
399
416
  }
417
+ return refreshIndex;
418
+ }
400
419
 
401
- for (const entry of workspaceChange.renamedPaths ?? []) {
420
+ _applyRenamedPaths(renamedPaths, refreshIndex) {
421
+ for (const entry of renamedPaths ?? []) {
402
422
  if (!entry?.oldPath || !entry?.newPath) {
403
423
  continue;
404
424
  }
405
425
  refreshIndex = this.onFileRenamed(entry.oldPath, entry.newPath, { refreshIndex: false }) || refreshIndex;
406
426
  }
427
+ return refreshIndex;
428
+ }
407
429
 
430
+ async _applyChangedPaths(changedPaths, previousEntries, nextEntries, refreshIndex) {
408
431
  for (const pathValue of changedPaths) {
409
432
  const existsNow = nextEntries.has(pathValue);
410
433
  const existedBefore = previousEntries.has(pathValue);
@@ -433,26 +456,22 @@ export class BacklinkIndex {
433
456
  refreshIndex = this.onFileCreated(pathValue, content, { refreshIndex: false }) || refreshIndex;
434
457
  }
435
458
  }
459
+ return refreshIndex;
460
+ }
436
461
 
437
- if (refreshIndex) {
438
- const sourcesToRefresh = new Set(impactedSources);
439
- changedPaths.forEach((pathValue) => {
440
- if (nextEntries.has(pathValue) && isMarkdownFilePath(pathValue)) {
441
- sourcesToRefresh.add(pathValue);
442
- }
443
- });
444
- (workspaceChange.renamedPaths ?? []).forEach((entry) => {
445
- if (entry?.newPath && isMarkdownFilePath(entry.newPath)) {
446
- sourcesToRefresh.add(entry.newPath);
447
- }
448
- });
449
-
450
- this._refreshWikiTargetIndex();
451
- await this._refreshImpactedSources(sourcesToRefresh, {
452
- nextEntries,
453
- renameMap,
454
- });
455
- }
462
+ _collectRefreshSources(impactedSources, changedPaths, renamedPaths, nextEntries) {
463
+ const sourcesToRefresh = new Set(impactedSources);
464
+ changedPaths.forEach((pathValue) => {
465
+ if (nextEntries.has(pathValue) && isMarkdownFilePath(pathValue)) {
466
+ sourcesToRefresh.add(pathValue);
467
+ }
468
+ });
469
+ (renamedPaths ?? []).forEach((entry) => {
470
+ if (entry?.newPath && isMarkdownFilePath(entry.newPath)) {
471
+ sourcesToRefresh.add(entry.newPath);
472
+ }
473
+ });
474
+ return sourcesToRefresh;
456
475
  }
457
476
 
458
477
  /**
@@ -70,8 +70,7 @@ export function createRequestHandler(
70
70
  workspaceMutationCoordinator,
71
71
  });
72
72
 
73
- return async function handleRequest(req, res) {
74
- const originalRequestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
73
+ function handleBasePathRedirect(req, res, originalRequestUrl) {
75
74
  if (
76
75
  config.basePath
77
76
  && (req.method === 'GET' || req.method === 'HEAD')
@@ -80,50 +79,56 @@ export function createRequestHandler(
80
79
  const location = `${config.basePath}/${originalRequestUrl.search}`;
81
80
  res.writeHead(308, { Location: location });
82
81
  res.end();
83
- return;
82
+ return true;
84
83
  }
84
+ return false;
85
+ }
85
86
 
86
- const requestUrl = createRequestUrlWithPathname(
87
- originalRequestUrl,
88
- stripBasePath(originalRequestUrl.pathname, config.basePath),
89
- );
90
- const isSameOriginWrite = isSameOriginWriteRequest(req, requestUrl);
91
-
92
- setHeaders(res, SECURITY_HEADERS);
93
-
94
- if (req.method === 'OPTIONS') {
95
- const requestedMethod = String(req.headers['access-control-request-method'] || '').toUpperCase();
96
- const preflightTargetsWrite = WRITE_METHODS.has(requestedMethod);
97
- if (preflightTargetsWrite && !isSameOriginWrite) {
98
- jsonResponse(req, res, 403, { error: 'Cross-origin write requests are not allowed' });
99
- return;
100
- }
87
+ function handleCorsPreflight(req, res, isSameOriginWrite) {
88
+ if (req.method !== 'OPTIONS') {
89
+ return false;
90
+ }
101
91
 
102
- if (isSameOriginWrite) {
103
- applyCorsHeaders(res, req.headers.origin);
104
- }
92
+ const requestedMethod = String(req.headers['access-control-request-method'] || '').toUpperCase();
93
+ const preflightTargetsWrite = WRITE_METHODS.has(requestedMethod);
94
+ if (preflightTargetsWrite && !isSameOriginWrite) {
95
+ jsonResponse(req, res, 403, { error: 'Cross-origin write requests are not allowed' });
96
+ return true;
97
+ }
105
98
 
106
- res.writeHead(204);
107
- res.end();
108
- return;
99
+ if (isSameOriginWrite) {
100
+ applyCorsHeaders(res, req.headers.origin);
109
101
  }
110
102
 
103
+ res.writeHead(204);
104
+ res.end();
105
+ return true;
106
+ }
107
+
108
+ function handleCorsWriteGuard(req, res, isSameOriginWrite) {
111
109
  if (WRITE_METHODS.has(req.method) && !isSameOriginWrite) {
112
110
  jsonResponse(req, res, 403, { error: 'Cross-origin write requests are not allowed' });
113
- return;
111
+ return true;
112
+ }
113
+ return false;
114
+ }
115
+
116
+ async function handleTestEndpoints(req, res, requestUrl) {
117
+ if (config.nodeEnv !== 'test') {
118
+ return false;
114
119
  }
115
120
 
116
- if (config.nodeEnv === 'test' && requestUrl.pathname === '/api/test/reset-state' && req.method === 'POST') {
121
+ if (requestUrl.pathname === '/api/test/reset-state' && req.method === 'POST') {
117
122
  await fileSystemSyncService?.resetForExternalStateChange?.();
118
123
  await roomRegistry?.reset?.();
119
124
  await backlinkIndex?.build?.();
120
125
  await workspaceMutationCoordinator?.initialize?.();
121
126
  await fileSystemSyncService?.resetForExternalStateChange?.();
122
127
  jsonResponse(req, res, 200, { ok: true });
123
- return;
128
+ return true;
124
129
  }
125
130
 
126
- if (config.nodeEnv === 'test' && requestUrl.pathname === '/api/test/hydrate-delay' && req.method === 'POST') {
131
+ if (requestUrl.pathname === '/api/test/hydrate-delay' && req.method === 'POST') {
127
132
  const body = await parseJsonBody(req).catch(() => ({}));
128
133
  testControls.wsRoomHydrateDelayMs = Math.max(0, Number(body?.delayMs) || 0);
129
134
  await fileSystemSyncService?.resetForExternalStateChange?.();
@@ -132,6 +137,36 @@ export function createRequestHandler(
132
137
  await workspaceMutationCoordinator?.initialize?.();
133
138
  await fileSystemSyncService?.resetForExternalStateChange?.();
134
139
  jsonResponse(req, res, 200, { delayMs: testControls.wsRoomHydrateDelayMs, ok: true });
140
+ return true;
141
+ }
142
+
143
+ return false;
144
+ }
145
+
146
+ return async function handleRequest(req, res) {
147
+ const originalRequestUrl = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
148
+
149
+ if (handleBasePathRedirect(req, res, originalRequestUrl)) {
150
+ return;
151
+ }
152
+
153
+ const requestUrl = createRequestUrlWithPathname(
154
+ originalRequestUrl,
155
+ stripBasePath(originalRequestUrl.pathname, config.basePath),
156
+ );
157
+ const isSameOriginWrite = isSameOriginWriteRequest(req, requestUrl);
158
+
159
+ setHeaders(res, SECURITY_HEADERS);
160
+
161
+ if (handleCorsPreflight(req, res, isSameOriginWrite)) {
162
+ return;
163
+ }
164
+
165
+ if (handleCorsWriteGuard(req, res, isSameOriginWrite)) {
166
+ return;
167
+ }
168
+
169
+ if (await handleTestEndpoints(req, res, requestUrl)) {
135
170
  return;
136
171
  }
137
172