collabmd 0.1.31 → 0.1.33

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.
Files changed (99) hide show
  1. package/dist/client/assets/{drawio-editor-B4sHHwWJ.js → drawio-editor-BMTwP5mD.js} +1 -1
  2. package/dist/client/assets/drawioEditor-D6clB5OH.js +2 -0
  3. package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Dzy_85Wp.js} +2 -2
  4. package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
  5. package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CAP9F8CB.js} +3 -3
  6. package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DD4AvzYP.js} +2 -2
  7. package/dist/client/assets/exportDocument-BbkN85c4.css +1 -0
  8. package/dist/client/assets/index-BIt5EZXz.css +1 -0
  9. package/dist/client/assets/{index-kPgEYvEX.js → index-Czg_KL-i.js} +2 -2
  10. package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  11. package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  12. package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  13. package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  14. package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  15. package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  16. package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  17. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  18. package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  19. package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  20. package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  21. package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  22. package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  23. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  24. package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  25. package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  26. package/dist/client/assets/main-DeowJOSp.js +1368 -0
  27. package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
  28. package/dist/client/assets/vault-api-client-p7h7cXKM.js +1 -0
  29. package/dist/client/drawio-editor.html +1 -1
  30. package/dist/client/excalidraw-editor.html +1 -1
  31. package/dist/client/export-document.html +2 -2
  32. package/dist/client/index.html +2 -2
  33. package/package.json +8 -5
  34. package/src/client/app/main-entry.js +2 -0
  35. package/src/client/application/app-shell/git-feature.js +28 -36
  36. package/src/client/application/app-shell/presence-feature.js +3 -1
  37. package/src/client/application/diagram-preview-hydrator.js +24 -15
  38. package/src/client/application/mermaid-preview-hydrator.js +121 -4
  39. package/src/client/application/plantuml-preview-hydrator.js +6 -16
  40. package/src/client/application/preview-renderer.js +11 -2
  41. package/src/client/bootstrap/collabmd-app-shell.js +182 -50
  42. package/src/client/domain/excalidraw-document-switch.js +3 -0
  43. package/src/client/excalidraw-editor.js +187 -84
  44. package/src/client/infrastructure/auth-client.js +4 -4
  45. package/src/client/infrastructure/backlinks-api-client.js +18 -0
  46. package/src/client/infrastructure/comment-thread-store.js +4 -0
  47. package/src/client/infrastructure/editor-session.js +6 -0
  48. package/src/client/infrastructure/editor-view-adapter.js +23 -0
  49. package/src/client/infrastructure/git-api-client.js +134 -0
  50. package/src/client/infrastructure/plantuml-api-client.js +21 -0
  51. package/src/client/infrastructure/vault-api-client.js +115 -0
  52. package/src/client/presentation/backlinks-panel.js +6 -9
  53. package/src/client/presentation/bases-preview-controller.js +1413 -34
  54. package/src/client/presentation/comments-panel.js +50 -5
  55. package/src/client/presentation/excalidraw-embed-controller.js +251 -15
  56. package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
  57. package/src/client/presentation/file-action-controller.js +97 -0
  58. package/src/client/presentation/file-explorer-controller.js +2 -0
  59. package/src/client/presentation/file-explorer-view.js +368 -0
  60. package/src/client/presentation/file-history-view-controller.js +41 -40
  61. package/src/client/presentation/git-diff-view-controller.js +70 -53
  62. package/src/client/presentation/git-panel-controller.js +12 -30
  63. package/src/client/styles/components/scrollbars.css +5 -0
  64. package/src/client/styles/export-document.css +45 -2
  65. package/src/client/styles/features/auth-gate.css +13 -24
  66. package/src/client/styles/features/comment-card.css +6 -0
  67. package/src/client/styles/features/comments-drawer.css +38 -18
  68. package/src/client/styles/features/diagram-preview.css +33 -0
  69. package/src/client/styles/features/embedded-preview.css +37 -11
  70. package/src/client/styles/features/preview-markdown.css +559 -39
  71. package/src/client/styles/foundation/themes.css +4 -0
  72. package/src/client/styles/foundation/tokens.css +4 -3
  73. package/src/client/styles/layout/sidebar.css +72 -0
  74. package/src/server/auth/auth-common.js +224 -0
  75. package/src/server/auth/auth-constants.js +30 -0
  76. package/src/server/auth/auth-strategies.js +320 -0
  77. package/src/server/auth/create-auth-service.js +24 -545
  78. package/src/server/domain/backlink-index.js +177 -5
  79. package/src/server/domain/bases/base-definition.js +382 -0
  80. package/src/server/domain/bases/base-expression-runtime.js +1196 -0
  81. package/src/server/domain/bases/base-index-snapshot-store.js +565 -0
  82. package/src/server/domain/bases/base-query-metadata.js +242 -0
  83. package/src/server/domain/bases/base-query-results.js +215 -0
  84. package/src/server/domain/bases/base-query-service.js +406 -1992
  85. package/src/server/domain/bases/base-transform.js +116 -0
  86. package/src/server/domain/collaboration/collaboration-room.js +36 -1
  87. package/src/server/domain/collaboration/room-client-state-store.js +1 -0
  88. package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
  89. package/src/server/infrastructure/http/create-vault-api-query-handler.js +161 -1
  90. package/src/server/infrastructure/http/http-response.js +56 -0
  91. package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
  92. package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
  93. package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
  94. package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
  95. package/dist/client/assets/index-D69HOZHr.css +0 -1
  96. package/dist/client/assets/main-DSxNTtD-.js +0 -1243
  97. package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
  98. package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
  99. /package/dist/client/assets/{exportDocument-CtZKWE2c.js → exportDocument-Zfwq1XVx.js} +0 -0
@@ -0,0 +1,565 @@
1
+ import { basename, dirname, extname } from 'node:path';
2
+
3
+ import { createWikiTargetIndex, resolveWikiTargetWithIndex } from '../../../domain/wiki-link-resolver.js';
4
+ import {
5
+ getVaultFileExtension,
6
+ isMarkdownFilePath,
7
+ stripVaultFileExtension,
8
+ } from '../../../domain/file-kind.js';
9
+ import { extractYamlFrontmatter } from '../../../domain/yaml-frontmatter.js';
10
+ import { mapWithConcurrency } from '../../shared/async-utils.js';
11
+ import { createLinkValue, dedupeLinkValues } from './base-expression-runtime.js';
12
+
13
+ const INTERNAL_LINK_RE = /(!)?\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]/g;
14
+ const INLINE_TAG_RE = /(^|[\s(])#([A-Za-z0-9/_-]+)/g;
15
+ const INDEX_READ_CONCURRENCY = 8;
16
+
17
+ function isPlainObject(value) {
18
+ return Object.prototype.toString.call(value) === '[object Object]';
19
+ }
20
+
21
+ function normalizeTags(value) {
22
+ if (Array.isArray(value)) {
23
+ return value
24
+ .map((entry) => String(entry ?? '').trim().replace(/^#/u, ''))
25
+ .filter(Boolean);
26
+ }
27
+
28
+ const normalized = String(value ?? '').trim().replace(/^#/u, '');
29
+ return normalized ? [normalized] : [];
30
+ }
31
+
32
+ function extractInlineTags(markdownText = '') {
33
+ const tags = new Set();
34
+ let match;
35
+ while ((match = INLINE_TAG_RE.exec(String(markdownText ?? ''))) !== null) {
36
+ tags.add(match[2]);
37
+ }
38
+ return [...tags];
39
+ }
40
+
41
+ function compareVaultPaths(left = '', right = '') {
42
+ return String(left).localeCompare(String(right), undefined, { sensitivity: 'base' });
43
+ }
44
+
45
+ function extractReferences(markdownText = '', wikiTargetIndex) {
46
+ const links = [];
47
+ const embeds = [];
48
+ let match;
49
+ while ((match = INTERNAL_LINK_RE.exec(String(markdownText ?? ''))) !== null) {
50
+ const isEmbed = Boolean(match[1]);
51
+ const rawTarget = String(match[2] ?? '').trim();
52
+ if (!rawTarget) {
53
+ continue;
54
+ }
55
+
56
+ const resolvedPath = resolveWikiTargetWithIndex(rawTarget, wikiTargetIndex);
57
+ const linkValue = createLinkValue(resolvedPath || rawTarget, {
58
+ exists: Boolean(resolvedPath),
59
+ rawTarget,
60
+ });
61
+ links.push(linkValue);
62
+ if (isEmbed) {
63
+ embeds.push(linkValue);
64
+ }
65
+ }
66
+ return { embeds, links };
67
+ }
68
+
69
+ function isWorkspaceFileEntry(entry = null) {
70
+ return Boolean(entry) && (entry.nodeType === 'file' || entry.type !== 'directory');
71
+ }
72
+
73
+ function listWorkspaceFilePaths(workspaceState = {}) {
74
+ if (Array.isArray(workspaceState?.filePaths)) {
75
+ return [...workspaceState.filePaths].sort(compareVaultPaths);
76
+ }
77
+
78
+ return Array.from(workspaceState?.entries?.values?.() ?? [])
79
+ .filter((entry) => isWorkspaceFileEntry(entry))
80
+ .map((entry) => entry.path)
81
+ .sort(compareVaultPaths);
82
+ }
83
+
84
+ function normalizeWikiTargetKey(target = '') {
85
+ const normalizedTarget = String(target ?? '').trim();
86
+ if (!normalizedTarget) {
87
+ return '';
88
+ }
89
+
90
+ return getVaultFileExtension(normalizedTarget)
91
+ ? normalizedTarget
92
+ : `${normalizedTarget}.md`;
93
+ }
94
+
95
+ function collectWikiTargetKeysForFilePath(filePath = '') {
96
+ const normalizedPath = String(filePath ?? '').trim();
97
+ if (!normalizedPath) {
98
+ return [];
99
+ }
100
+
101
+ const segments = normalizedPath.split('/').filter(Boolean);
102
+ const keys = [];
103
+ for (let index = 0; index < segments.length; index += 1) {
104
+ keys.push(segments.slice(index).join('/'));
105
+ }
106
+
107
+ if (isMarkdownFilePath(normalizedPath)) {
108
+ const rawSegments = normalizedPath.replace(/\.md$/i, '').split('/').filter(Boolean);
109
+ for (let index = 0; index < rawSegments.length; index += 1) {
110
+ keys.push(rawSegments.slice(index).join('/'));
111
+ }
112
+ }
113
+
114
+ return [...new Set(keys)];
115
+ }
116
+
117
+ function createBacklinkEntry(sourcePath = '') {
118
+ return createLinkValue(sourcePath, {
119
+ display: basename(sourcePath),
120
+ exists: true,
121
+ });
122
+ }
123
+
124
+ function addRawTargetSourceContribution(snapshot, sourcePath, rawTargetKey) {
125
+ if (!rawTargetKey) {
126
+ return;
127
+ }
128
+
129
+ const sources = snapshot.rawTargetSourcesByKey.get(rawTargetKey) ?? new Set();
130
+ sources.add(sourcePath);
131
+ snapshot.rawTargetSourcesByKey.set(rawTargetKey, sources);
132
+ }
133
+
134
+ export class BaseIndexSnapshotStore {
135
+ constructor({
136
+ vaultFileStore,
137
+ workspaceStateProvider = null,
138
+ workspaceStateSynchronizer = null,
139
+ }) {
140
+ this.vaultFileStore = vaultFileStore;
141
+ this.workspaceStateProvider = workspaceStateProvider;
142
+ this.workspaceStateSynchronizer = workspaceStateSynchronizer;
143
+ this.indexSnapshot = null;
144
+ this.lastWorkspaceState = null;
145
+ }
146
+
147
+ async getWorkspaceState() {
148
+ const workspaceState = await this.workspaceStateProvider?.();
149
+ if (workspaceState) {
150
+ this.lastWorkspaceState = workspaceState;
151
+ return workspaceState;
152
+ }
153
+
154
+ if (this.lastWorkspaceState) {
155
+ return this.lastWorkspaceState;
156
+ }
157
+
158
+ const scannedWorkspaceState = await this.vaultFileStore.scanWorkspaceState();
159
+ this.lastWorkspaceState = scannedWorkspaceState;
160
+ return scannedWorkspaceState;
161
+ }
162
+
163
+ createSnapshotRow(filePath, workspaceState, wikiTargetIndex, markdownContent = null) {
164
+ const metadata = workspaceState.metadata.get(filePath) ?? null;
165
+ const frontmatter = markdownContent ? extractYamlFrontmatter(markdownContent) : null;
166
+ const noteProperties = isPlainObject(frontmatter?.data) ? frontmatter.data : {};
167
+ const bodyMarkdown = frontmatter?.bodyMarkdown ?? markdownContent ?? '';
168
+ const references = extractReferences(bodyMarkdown, wikiTargetIndex);
169
+ const tags = [...new Set([
170
+ ...normalizeTags(noteProperties.tags),
171
+ ...extractInlineTags(bodyMarkdown),
172
+ ])];
173
+ const fileValue = {
174
+ __baseType: 'file',
175
+ backlinks: [],
176
+ basename: stripVaultFileExtension(basename(filePath)),
177
+ ctime: Number.isFinite(metadata?.ctimeMs) ? new Date(metadata.ctimeMs) : null,
178
+ embeds: references.embeds,
179
+ ext: extname(filePath).replace(/^\./u, ''),
180
+ folder: dirname(filePath) === '.' ? '' : dirname(filePath).replace(/\\/g, '/'),
181
+ links: references.links,
182
+ mtime: Number.isFinite(metadata?.mtimeMs) ? new Date(metadata.mtimeMs) : null,
183
+ name: basename(filePath),
184
+ path: filePath,
185
+ properties: noteProperties,
186
+ size: Number(metadata?.size ?? 0) || 0,
187
+ tags,
188
+ };
189
+
190
+ return {
191
+ forwardLinks: new Set(
192
+ references.links
193
+ .map((link) => link?.path)
194
+ .filter((targetPath) => targetPath && targetPath !== filePath),
195
+ ),
196
+ rawTargetKeys: new Set(
197
+ references.links
198
+ .map((link) => normalizeWikiTargetKey(link?.rawTarget))
199
+ .filter(Boolean),
200
+ ),
201
+ row: {
202
+ file: fileValue,
203
+ noteProperties,
204
+ path: filePath,
205
+ },
206
+ };
207
+ }
208
+
209
+ rebuildBacklinks(snapshot) {
210
+ snapshot.backlinkSourcesByTarget = new Map();
211
+ snapshot.backlinksByPath = new Map(snapshot.filePaths.map((filePath) => [filePath, []]));
212
+
213
+ snapshot.rowsByPath.forEach((row, filePath) => {
214
+ const forwardLinks = snapshot.forwardLinksByPath.get(filePath) ?? new Set();
215
+ forwardLinks.forEach((targetPath) => {
216
+ if (!snapshot.rowsByPath.has(targetPath) || targetPath === row.file.path) {
217
+ return;
218
+ }
219
+
220
+ const sources = snapshot.backlinkSourcesByTarget.get(targetPath) ?? new Set();
221
+ sources.add(filePath);
222
+ snapshot.backlinkSourcesByTarget.set(targetPath, sources);
223
+ });
224
+ });
225
+
226
+ this.refreshSerializedBacklinks(snapshot, snapshot.filePaths);
227
+ }
228
+
229
+ removeSourceBacklinkContributions(snapshot, sourcePath, affectedTargetPaths = new Set()) {
230
+ const forwardLinks = snapshot.forwardLinksByPath.get(sourcePath) ?? new Set();
231
+ forwardLinks.forEach((targetPath) => {
232
+ const sources = snapshot.backlinkSourcesByTarget.get(targetPath);
233
+ if (!sources) {
234
+ return;
235
+ }
236
+
237
+ sources.delete(sourcePath);
238
+ if (sources.size === 0) {
239
+ snapshot.backlinkSourcesByTarget.delete(targetPath);
240
+ }
241
+ affectedTargetPaths.add(targetPath);
242
+ });
243
+ }
244
+
245
+ removeRawTargetSourceContributions(snapshot, sourcePath) {
246
+ const rawTargetKeys = snapshot.rawTargetKeysBySourcePath.get(sourcePath) ?? new Set();
247
+ rawTargetKeys.forEach((rawTargetKey) => {
248
+ const sources = snapshot.rawTargetSourcesByKey.get(rawTargetKey);
249
+ if (!sources) {
250
+ return;
251
+ }
252
+
253
+ sources.delete(sourcePath);
254
+ if (sources.size === 0) {
255
+ snapshot.rawTargetSourcesByKey.delete(rawTargetKey);
256
+ }
257
+ });
258
+ snapshot.rawTargetKeysBySourcePath.delete(sourcePath);
259
+ }
260
+
261
+ addRawTargetSourceContributions(snapshot, sourcePath, rawTargetKeys = new Set()) {
262
+ snapshot.rawTargetKeysBySourcePath.set(sourcePath, rawTargetKeys);
263
+ rawTargetKeys.forEach((rawTargetKey) => {
264
+ addRawTargetSourceContribution(snapshot, sourcePath, rawTargetKey);
265
+ });
266
+ }
267
+
268
+ addSourceBacklinkContributions(snapshot, sourcePath, affectedTargetPaths = new Set()) {
269
+ if (!snapshot.rowsByPath.has(sourcePath)) {
270
+ return;
271
+ }
272
+
273
+ const forwardLinks = snapshot.forwardLinksByPath.get(sourcePath) ?? new Set();
274
+ forwardLinks.forEach((targetPath) => {
275
+ if (!snapshot.rowsByPath.has(targetPath) || targetPath === sourcePath) {
276
+ return;
277
+ }
278
+
279
+ const sources = snapshot.backlinkSourcesByTarget.get(targetPath) ?? new Set();
280
+ sources.add(sourcePath);
281
+ snapshot.backlinkSourcesByTarget.set(targetPath, sources);
282
+ affectedTargetPaths.add(targetPath);
283
+ });
284
+ }
285
+
286
+ refreshSerializedBacklinks(snapshot, targetPaths = []) {
287
+ Array.from(new Set(targetPaths)).forEach((targetPath) => {
288
+ if (!snapshot.rowsByPath.has(targetPath)) {
289
+ snapshot.backlinkSourcesByTarget.delete(targetPath);
290
+ snapshot.backlinksByPath.delete(targetPath);
291
+ return;
292
+ }
293
+
294
+ const sources = snapshot.backlinkSourcesByTarget.get(targetPath) ?? new Set();
295
+ const backlinks = dedupeLinkValues(
296
+ Array.from(sources, (sourcePath) => createBacklinkEntry(sourcePath)),
297
+ );
298
+ snapshot.backlinksByPath.set(targetPath, backlinks);
299
+ const row = snapshot.rowsByPath.get(targetPath);
300
+ if (row?.file) {
301
+ row.file.backlinks = backlinks;
302
+ }
303
+ });
304
+ }
305
+
306
+ async buildIndexSnapshot(workspaceState = null) {
307
+ const resolvedWorkspaceState = workspaceState ?? await this.getWorkspaceState();
308
+ const fileEntries = listWorkspaceFilePaths(resolvedWorkspaceState);
309
+ const wikiTargetIndex = createWikiTargetIndex(fileEntries);
310
+ const markdownContents = new Map();
311
+
312
+ await mapWithConcurrency(resolvedWorkspaceState.markdownPaths ?? [], INDEX_READ_CONCURRENCY, async (filePath) => {
313
+ markdownContents.set(filePath, await this.vaultFileStore.readMarkdownFile(filePath));
314
+ });
315
+
316
+ const rowsByPath = new Map();
317
+ const forwardLinksByPath = new Map();
318
+ const rawTargetKeysBySourcePath = new Map();
319
+ const rawTargetSourcesByKey = new Map();
320
+ fileEntries.forEach((filePath) => {
321
+ const markdownContent = markdownContents.get(filePath) ?? null;
322
+ const rowRecord = this.createSnapshotRow(filePath, resolvedWorkspaceState, wikiTargetIndex, markdownContent);
323
+ rowsByPath.set(filePath, rowRecord.row);
324
+ forwardLinksByPath.set(filePath, rowRecord.forwardLinks);
325
+ rawTargetKeysBySourcePath.set(filePath, rowRecord.rawTargetKeys);
326
+ rowRecord.rawTargetKeys.forEach((rawTargetKey) => {
327
+ addRawTargetSourceContribution({
328
+ rawTargetSourcesByKey,
329
+ }, filePath, rawTargetKey);
330
+ });
331
+ });
332
+
333
+ const snapshot = {
334
+ backlinkSourcesByTarget: new Map(),
335
+ backlinksByPath: new Map(),
336
+ filePaths: fileEntries,
337
+ forwardLinksByPath,
338
+ rawTargetKeysBySourcePath,
339
+ rawTargetSourcesByKey,
340
+ rowsByPath,
341
+ scannedAt: resolvedWorkspaceState.scannedAt,
342
+ wikiTargetIndex,
343
+ workspaceState: resolvedWorkspaceState,
344
+ };
345
+ this.rebuildBacklinks(snapshot);
346
+ this.indexSnapshot = snapshot;
347
+ this.lastWorkspaceState = resolvedWorkspaceState;
348
+ return snapshot;
349
+ }
350
+
351
+ async synchronizeWorkspaceState() {
352
+ await this.workspaceStateSynchronizer?.();
353
+ }
354
+
355
+ removeSnapshotPath(snapshot, filePath) {
356
+ this.removeRawTargetSourceContributions(snapshot, filePath);
357
+ snapshot.rowsByPath.delete(filePath);
358
+ snapshot.forwardLinksByPath.delete(filePath);
359
+ snapshot.backlinkSourcesByTarget.delete(filePath);
360
+ snapshot.backlinksByPath.delete(filePath);
361
+ snapshot.filePaths = snapshot.filePaths.filter((candidatePath) => candidatePath !== filePath);
362
+ }
363
+
364
+ upsertSnapshotPath(snapshot, filePath) {
365
+ if (snapshot.filePaths.includes(filePath)) {
366
+ return;
367
+ }
368
+
369
+ snapshot.filePaths.push(filePath);
370
+ snapshot.filePaths.sort(compareVaultPaths);
371
+ }
372
+
373
+ collectImpactedSourcesForMembershipChanges(snapshot, pathValues = []) {
374
+ const affectedTargetKeys = new Set();
375
+ pathValues.forEach((pathValue) => {
376
+ collectWikiTargetKeysForFilePath(pathValue).forEach((targetKey) => {
377
+ affectedTargetKeys.add(targetKey);
378
+ });
379
+ });
380
+
381
+ if (affectedTargetKeys.size === 0) {
382
+ return new Set();
383
+ }
384
+
385
+ const impactedPaths = new Set();
386
+ affectedTargetKeys.forEach((targetKey) => {
387
+ const sourcePaths = snapshot.rawTargetSourcesByKey.get(targetKey);
388
+ sourcePaths?.forEach((filePath) => {
389
+ impactedPaths.add(filePath);
390
+ });
391
+ });
392
+ return impactedPaths;
393
+ }
394
+
395
+ async refreshSnapshotRows(snapshot, workspaceState, pathValues = []) {
396
+ const filePathsToRefresh = Array.from(new Set(pathValues))
397
+ .filter((pathValue) => {
398
+ const entry = workspaceState?.entries?.get(pathValue);
399
+ return pathValue && isWorkspaceFileEntry(entry);
400
+ });
401
+
402
+ await mapWithConcurrency(filePathsToRefresh, INDEX_READ_CONCURRENCY, async (filePath) => {
403
+ const markdownContent = isMarkdownFilePath(filePath)
404
+ ? await this.vaultFileStore.readMarkdownFile(filePath)
405
+ : null;
406
+ const rowRecord = this.createSnapshotRow(filePath, workspaceState, snapshot.wikiTargetIndex, markdownContent);
407
+ this.removeRawTargetSourceContributions(snapshot, filePath);
408
+ snapshot.rowsByPath.set(filePath, rowRecord.row);
409
+ snapshot.forwardLinksByPath.set(filePath, rowRecord.forwardLinks);
410
+ this.addRawTargetSourceContributions(snapshot, filePath, rowRecord.rawTargetKeys);
411
+ });
412
+ }
413
+
414
+ async ensureIndexSnapshot({ basePath = '', sourcePath = '' } = {}) {
415
+ await this.synchronizeWorkspaceState();
416
+ let workspaceState = await this.getWorkspaceState();
417
+ const requiresFreshScan = [basePath, sourcePath]
418
+ .filter(Boolean)
419
+ .some((pathValue) => !workspaceState?.entries?.has?.(pathValue));
420
+ if (requiresFreshScan) {
421
+ workspaceState = await this.vaultFileStore.scanWorkspaceState();
422
+ this.lastWorkspaceState = workspaceState;
423
+ if (this.indexSnapshot?.scannedAt !== workspaceState?.scannedAt) {
424
+ this.indexSnapshot = null;
425
+ }
426
+ }
427
+
428
+ if (this.indexSnapshot?.scannedAt === workspaceState?.scannedAt) {
429
+ return this.indexSnapshot;
430
+ }
431
+
432
+ return this.buildIndexSnapshot(workspaceState);
433
+ }
434
+
435
+ async initializeFromWorkspaceState(workspaceState = null) {
436
+ this.lastWorkspaceState = workspaceState ?? null;
437
+ if (this.indexSnapshot && workspaceState && this.indexSnapshot.scannedAt !== workspaceState.scannedAt) {
438
+ this.indexSnapshot = null;
439
+ }
440
+ return this.indexSnapshot;
441
+ }
442
+
443
+ async applyWorkspaceChange(workspaceChange = {}, {
444
+ previousState = null,
445
+ nextState = null,
446
+ } = {}) {
447
+ this.lastWorkspaceState = nextState ?? this.lastWorkspaceState;
448
+ if (!this.indexSnapshot) {
449
+ return null;
450
+ }
451
+
452
+ if (!previousState || this.indexSnapshot.scannedAt !== previousState.scannedAt) {
453
+ this.indexSnapshot = null;
454
+ return null;
455
+ }
456
+
457
+ const changedPaths = Array.from(new Set(workspaceChange.changedPaths ?? []));
458
+ const deletedFilePaths = (workspaceChange.deletedPaths ?? []).filter((pathValue) => (
459
+ isWorkspaceFileEntry(previousState?.entries?.get(pathValue))
460
+ ));
461
+ const renamedFileEntries = (workspaceChange.renamedPaths ?? []).filter((entry) => (
462
+ isWorkspaceFileEntry(previousState?.entries?.get(entry?.oldPath))
463
+ || isWorkspaceFileEntry(nextState?.entries?.get(entry?.newPath))
464
+ ));
465
+ const createdFilePaths = changedPaths.filter((pathValue) => (
466
+ isWorkspaceFileEntry(nextState?.entries?.get(pathValue))
467
+ && !isWorkspaceFileEntry(previousState?.entries?.get(pathValue))
468
+ ));
469
+ const removedChangedFilePaths = changedPaths.filter((pathValue) => (
470
+ !isWorkspaceFileEntry(nextState?.entries?.get(pathValue))
471
+ && isWorkspaceFileEntry(previousState?.entries?.get(pathValue))
472
+ ));
473
+ const membershipChanged = deletedFilePaths.length > 0
474
+ || renamedFileEntries.length > 0
475
+ || createdFilePaths.length > 0
476
+ || removedChangedFilePaths.length > 0;
477
+
478
+ const markdownPathsToRefresh = changedPaths.filter((pathValue) => (
479
+ isWorkspaceFileEntry(previousState?.entries?.get(pathValue))
480
+ && isWorkspaceFileEntry(nextState?.entries?.get(pathValue))
481
+ ));
482
+
483
+ if (!membershipChanged && markdownPathsToRefresh.length === 0) {
484
+ this.indexSnapshot.workspaceState = nextState ?? this.indexSnapshot.workspaceState;
485
+ this.indexSnapshot.scannedAt = nextState?.scannedAt ?? this.indexSnapshot.scannedAt;
486
+ return this.indexSnapshot;
487
+ }
488
+
489
+ const snapshot = this.indexSnapshot;
490
+ const affectedTargetPaths = new Set();
491
+ const affectedSourcePaths = new Set(markdownPathsToRefresh);
492
+ if (membershipChanged) {
493
+ const membershipAffectedPaths = [
494
+ ...deletedFilePaths,
495
+ ...removedChangedFilePaths,
496
+ ...createdFilePaths,
497
+ ...renamedFileEntries.flatMap((entry) => [entry.oldPath, entry.newPath]),
498
+ ];
499
+ const impactedSourcePaths = this.collectImpactedSourcesForMembershipChanges(snapshot, membershipAffectedPaths);
500
+ impactedSourcePaths.forEach((pathValue) => affectedSourcePaths.add(pathValue));
501
+ [...deletedFilePaths, ...removedChangedFilePaths].forEach((pathValue) => {
502
+ if (isMarkdownFilePath(pathValue)) {
503
+ affectedSourcePaths.add(pathValue);
504
+ }
505
+ affectedTargetPaths.add(pathValue);
506
+ });
507
+ renamedFileEntries.forEach((entry) => {
508
+ if (isMarkdownFilePath(entry.oldPath)) {
509
+ affectedSourcePaths.add(entry.oldPath);
510
+ }
511
+ if (isMarkdownFilePath(entry.newPath)) {
512
+ affectedSourcePaths.add(entry.newPath);
513
+ }
514
+ affectedTargetPaths.add(entry.oldPath);
515
+ affectedTargetPaths.add(entry.newPath);
516
+ });
517
+ createdFilePaths.forEach((pathValue) => {
518
+ if (isMarkdownFilePath(pathValue)) {
519
+ affectedSourcePaths.add(pathValue);
520
+ }
521
+ affectedTargetPaths.add(pathValue);
522
+ });
523
+
524
+ affectedSourcePaths.forEach((pathValue) => {
525
+ this.removeSourceBacklinkContributions(snapshot, pathValue, affectedTargetPaths);
526
+ });
527
+
528
+ [...deletedFilePaths, ...removedChangedFilePaths].forEach((pathValue) => {
529
+ this.removeSnapshotPath(snapshot, pathValue);
530
+ });
531
+ renamedFileEntries.forEach((entry) => {
532
+ this.removeSnapshotPath(snapshot, entry.oldPath);
533
+ });
534
+ [...createdFilePaths, ...renamedFileEntries.map((entry) => entry.newPath)].forEach((pathValue) => {
535
+ const nextEntry = nextState?.entries?.get(pathValue);
536
+ if (isWorkspaceFileEntry(nextEntry)) {
537
+ this.upsertSnapshotPath(snapshot, pathValue);
538
+ }
539
+ });
540
+
541
+ snapshot.wikiTargetIndex = createWikiTargetIndex(snapshot.filePaths);
542
+ await this.refreshSnapshotRows(snapshot, nextState, [
543
+ ...createdFilePaths,
544
+ ...renamedFileEntries.map((entry) => entry.newPath),
545
+ ...affectedSourcePaths,
546
+ ]);
547
+ } else {
548
+ affectedSourcePaths.forEach((pathValue) => {
549
+ this.removeSourceBacklinkContributions(snapshot, pathValue, affectedTargetPaths);
550
+ });
551
+ await this.refreshSnapshotRows(snapshot, nextState, [...affectedSourcePaths]);
552
+ }
553
+
554
+ affectedSourcePaths.forEach((pathValue) => {
555
+ this.addSourceBacklinkContributions(snapshot, pathValue, affectedTargetPaths);
556
+ });
557
+ snapshot.workspaceState = nextState;
558
+ snapshot.scannedAt = nextState?.scannedAt ?? snapshot.scannedAt;
559
+ this.refreshSerializedBacklinks(snapshot, [
560
+ ...snapshot.filePaths.filter((filePath) => !snapshot.backlinksByPath.has(filePath)),
561
+ ...affectedTargetPaths,
562
+ ]);
563
+ return snapshot;
564
+ }
565
+ }