synapse-db 0.0.0-stage → 0.2.3
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/LICENSE +21 -0
- package/README.md +46 -2
- package/dist/canvasExporter.d.ts +32 -0
- package/dist/collection.d.ts +52 -0
- package/dist/connectome.d.ts +85 -0
- package/dist/engine.d.ts +59 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +942 -0
- package/dist/migrator.d.ts +24 -0
- package/dist/serializer.d.ts +25 -0
- package/dist/types.d.ts +62 -0
- package/package.json +42 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// engine.ts
|
|
3
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, unlinkSync as unlinkSync2, watch } from "fs";
|
|
4
|
+
import { basename, join, resolve } from "path";
|
|
5
|
+
|
|
6
|
+
// connectome.ts
|
|
7
|
+
class SynapseConnectome {
|
|
8
|
+
documents = new Map;
|
|
9
|
+
collectionDocIds = new Map;
|
|
10
|
+
outgoingEdges = new Map;
|
|
11
|
+
incomingEdges = new Map;
|
|
12
|
+
edgeMeta = new Map;
|
|
13
|
+
tokenInvertedIndex = new Map;
|
|
14
|
+
documentTokens = new Map;
|
|
15
|
+
constructor() {}
|
|
16
|
+
static makeNodeId(collection, id) {
|
|
17
|
+
return `${collection}/${id}`;
|
|
18
|
+
}
|
|
19
|
+
clear() {
|
|
20
|
+
this.documents.clear();
|
|
21
|
+
this.collectionDocIds.clear();
|
|
22
|
+
this.outgoingEdges.clear();
|
|
23
|
+
this.incomingEdges.clear();
|
|
24
|
+
this.edgeMeta.clear();
|
|
25
|
+
this.tokenInvertedIndex.clear();
|
|
26
|
+
this.documentTokens.clear();
|
|
27
|
+
}
|
|
28
|
+
putDocument(doc) {
|
|
29
|
+
const nodeId = SynapseConnectome.makeNodeId(doc.collection, doc.id);
|
|
30
|
+
this.removeDocumentOwnedIndices(nodeId);
|
|
31
|
+
this.documents.set(nodeId, doc);
|
|
32
|
+
let colSet = this.collectionDocIds.get(doc.collection);
|
|
33
|
+
if (!colSet) {
|
|
34
|
+
colSet = new Set;
|
|
35
|
+
this.collectionDocIds.set(doc.collection, colSet);
|
|
36
|
+
}
|
|
37
|
+
colSet.add(doc.id);
|
|
38
|
+
for (const link of doc.wikilinks) {
|
|
39
|
+
const targetNodeId = SynapseConnectome.makeNodeId(link.targetCollection || doc.collection, link.targetId);
|
|
40
|
+
let outSet = this.outgoingEdges.get(nodeId);
|
|
41
|
+
if (!outSet) {
|
|
42
|
+
outSet = new Set;
|
|
43
|
+
this.outgoingEdges.set(nodeId, outSet);
|
|
44
|
+
}
|
|
45
|
+
outSet.add(targetNodeId);
|
|
46
|
+
let inSet = this.incomingEdges.get(targetNodeId);
|
|
47
|
+
if (!inSet) {
|
|
48
|
+
inSet = new Set;
|
|
49
|
+
this.incomingEdges.set(targetNodeId, inSet);
|
|
50
|
+
}
|
|
51
|
+
inSet.add(nodeId);
|
|
52
|
+
const edgeKey = `${nodeId}->${targetNodeId}`;
|
|
53
|
+
this.edgeMeta.set(edgeKey, {
|
|
54
|
+
fromNodeId: nodeId,
|
|
55
|
+
toNodeId: targetNodeId,
|
|
56
|
+
alias: link.alias
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
this.indexDocumentText(nodeId, doc);
|
|
60
|
+
}
|
|
61
|
+
removeDocument(collection, id) {
|
|
62
|
+
const nodeId = SynapseConnectome.makeNodeId(collection, id);
|
|
63
|
+
if (!this.documents.has(nodeId))
|
|
64
|
+
return false;
|
|
65
|
+
this.removeDocumentOwnedIndices(nodeId);
|
|
66
|
+
this.documents.delete(nodeId);
|
|
67
|
+
const colSet = this.collectionDocIds.get(collection);
|
|
68
|
+
if (colSet) {
|
|
69
|
+
colSet.delete(id);
|
|
70
|
+
if (colSet.size === 0) {
|
|
71
|
+
this.collectionDocIds.delete(collection);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
removeDocumentOwnedIndices(nodeId) {
|
|
77
|
+
const outSet = this.outgoingEdges.get(nodeId);
|
|
78
|
+
if (outSet) {
|
|
79
|
+
for (const targetNodeId of outSet) {
|
|
80
|
+
const inSet = this.incomingEdges.get(targetNodeId);
|
|
81
|
+
if (inSet) {
|
|
82
|
+
inSet.delete(nodeId);
|
|
83
|
+
if (inSet.size === 0) {
|
|
84
|
+
this.incomingEdges.delete(targetNodeId);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
this.edgeMeta.delete(`${nodeId}->${targetNodeId}`);
|
|
88
|
+
}
|
|
89
|
+
this.outgoingEdges.delete(nodeId);
|
|
90
|
+
}
|
|
91
|
+
const oldTokens = this.documentTokens.get(nodeId);
|
|
92
|
+
if (oldTokens) {
|
|
93
|
+
for (const token of oldTokens) {
|
|
94
|
+
const nodeSet = this.tokenInvertedIndex.get(token);
|
|
95
|
+
if (nodeSet) {
|
|
96
|
+
nodeSet.delete(nodeId);
|
|
97
|
+
if (nodeSet.size === 0) {
|
|
98
|
+
this.tokenInvertedIndex.delete(token);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.documentTokens.delete(nodeId);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
getDocument(collection, id) {
|
|
106
|
+
const nodeId = SynapseConnectome.makeNodeId(collection, id);
|
|
107
|
+
return this.documents.get(nodeId) || null;
|
|
108
|
+
}
|
|
109
|
+
getOutgoingDocuments(collection, id, targetCollectionFilter) {
|
|
110
|
+
const nodeId = SynapseConnectome.makeNodeId(collection, id);
|
|
111
|
+
const targetNodeIds = this.outgoingEdges.get(nodeId);
|
|
112
|
+
if (!targetNodeIds)
|
|
113
|
+
return [];
|
|
114
|
+
const results = [];
|
|
115
|
+
for (const tNodeId of targetNodeIds) {
|
|
116
|
+
if (targetCollectionFilter && !tNodeId.startsWith(`${targetCollectionFilter}/`)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const doc = this.documents.get(tNodeId);
|
|
120
|
+
if (doc)
|
|
121
|
+
results.push(doc);
|
|
122
|
+
}
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
getIncomingDocuments(collection, id, sourceCollectionFilter) {
|
|
126
|
+
const nodeId = SynapseConnectome.makeNodeId(collection, id);
|
|
127
|
+
const sourceNodeIds = this.incomingEdges.get(nodeId);
|
|
128
|
+
if (!sourceNodeIds)
|
|
129
|
+
return [];
|
|
130
|
+
const results = [];
|
|
131
|
+
for (const sNodeId of sourceNodeIds) {
|
|
132
|
+
if (sourceCollectionFilter && !sNodeId.startsWith(`${sourceCollectionFilter}/`)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const doc = this.documents.get(sNodeId);
|
|
136
|
+
if (doc)
|
|
137
|
+
results.push(doc);
|
|
138
|
+
}
|
|
139
|
+
return results;
|
|
140
|
+
}
|
|
141
|
+
verifyGraphInvariants() {
|
|
142
|
+
const brokenOutgoing = [];
|
|
143
|
+
const brokenIncoming = [];
|
|
144
|
+
const ghostTokens = [];
|
|
145
|
+
for (const [u, outTargets] of this.outgoingEdges) {
|
|
146
|
+
for (const v of outTargets) {
|
|
147
|
+
const inSources = this.incomingEdges.get(v);
|
|
148
|
+
if (!inSources || !inSources.has(u)) {
|
|
149
|
+
brokenOutgoing.push(`${u}->${v}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const [v, inSources] of this.incomingEdges) {
|
|
154
|
+
for (const u of inSources) {
|
|
155
|
+
const outTargets = this.outgoingEdges.get(u);
|
|
156
|
+
if (!outTargets || !outTargets.has(v)) {
|
|
157
|
+
brokenIncoming.push(`${u}->${v}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const [token, nodeSet] of this.tokenInvertedIndex) {
|
|
162
|
+
for (const nodeId of nodeSet) {
|
|
163
|
+
if (!this.documents.has(nodeId)) {
|
|
164
|
+
ghostTokens.push(`${token}@${nodeId}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
ok: brokenOutgoing.length === 0 && brokenIncoming.length === 0 && ghostTokens.length === 0,
|
|
170
|
+
brokenOutgoing,
|
|
171
|
+
brokenIncoming,
|
|
172
|
+
ghostTokens
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
getAllEdges() {
|
|
176
|
+
return Array.from(this.edgeMeta.values());
|
|
177
|
+
}
|
|
178
|
+
getAllDocuments() {
|
|
179
|
+
return Array.from(this.documents.values());
|
|
180
|
+
}
|
|
181
|
+
queryCollection(collection, options) {
|
|
182
|
+
const ids = this.collectionDocIds.get(collection);
|
|
183
|
+
if (!ids)
|
|
184
|
+
return [];
|
|
185
|
+
const docs = [];
|
|
186
|
+
for (const id of ids) {
|
|
187
|
+
const doc = this.documents.get(SynapseConnectome.makeNodeId(collection, id));
|
|
188
|
+
if (!doc)
|
|
189
|
+
continue;
|
|
190
|
+
if (options?.filter && !options.filter(doc))
|
|
191
|
+
continue;
|
|
192
|
+
docs.push(doc);
|
|
193
|
+
}
|
|
194
|
+
if (options?.sortBy) {
|
|
195
|
+
docs.sort(options.sortBy);
|
|
196
|
+
}
|
|
197
|
+
const offset = options?.offset || 0;
|
|
198
|
+
const limit = options?.limit !== undefined ? options.limit : docs.length;
|
|
199
|
+
return docs.slice(offset, offset + limit);
|
|
200
|
+
}
|
|
201
|
+
search(query, collectionFilter) {
|
|
202
|
+
const tokens = this.tokenize(query);
|
|
203
|
+
if (tokens.length === 0)
|
|
204
|
+
return [];
|
|
205
|
+
const scoreMap = new Map;
|
|
206
|
+
for (const token of tokens) {
|
|
207
|
+
const matchedNodeIds = this.tokenInvertedIndex.get(token);
|
|
208
|
+
if (!matchedNodeIds)
|
|
209
|
+
continue;
|
|
210
|
+
for (const nodeId of matchedNodeIds) {
|
|
211
|
+
if (collectionFilter && !nodeId.startsWith(`${collectionFilter}/`)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
let entry = scoreMap.get(nodeId);
|
|
215
|
+
if (!entry) {
|
|
216
|
+
entry = { score: 0, matchedTerms: new Set };
|
|
217
|
+
scoreMap.set(nodeId, entry);
|
|
218
|
+
}
|
|
219
|
+
entry.score += 1;
|
|
220
|
+
entry.matchedTerms.add(token);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const results = [];
|
|
224
|
+
for (const [nodeId, { score, matchedTerms }] of scoreMap) {
|
|
225
|
+
const doc = this.documents.get(nodeId);
|
|
226
|
+
if (!doc)
|
|
227
|
+
continue;
|
|
228
|
+
results.push({
|
|
229
|
+
document: doc,
|
|
230
|
+
score,
|
|
231
|
+
matchedTerms: Array.from(matchedTerms)
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return results.sort((a, b) => b.score - a.score);
|
|
235
|
+
}
|
|
236
|
+
tokenize(text) {
|
|
237
|
+
const normalized = text.toLowerCase().replace(/[\r\n\t]/g, " ");
|
|
238
|
+
const words = normalized.split(/[^a-zA-Z0-9\uAC00-\uD7A3]+/).filter((w) => w.length > 0);
|
|
239
|
+
const tokens = new Set;
|
|
240
|
+
for (const word of words) {
|
|
241
|
+
tokens.add(word);
|
|
242
|
+
if (/[\uAC00-\uD7A3]/.test(word) && word.length >= 2) {
|
|
243
|
+
for (let i = 0;i < word.length - 1; i++) {
|
|
244
|
+
tokens.add(word.slice(i, i + 2));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return Array.from(tokens);
|
|
249
|
+
}
|
|
250
|
+
indexDocumentText(nodeId, doc) {
|
|
251
|
+
const rawDataStr = JSON.stringify(doc.data);
|
|
252
|
+
const fullText = `${doc.id} ${doc.collection} ${rawDataStr} ${doc.body}`;
|
|
253
|
+
const tokens = this.tokenize(fullText);
|
|
254
|
+
const docTokenSet = new Set(tokens);
|
|
255
|
+
this.documentTokens.set(nodeId, docTokenSet);
|
|
256
|
+
for (const token of docTokenSet) {
|
|
257
|
+
let set = this.tokenInvertedIndex.get(token);
|
|
258
|
+
if (!set) {
|
|
259
|
+
set = new Set;
|
|
260
|
+
this.tokenInvertedIndex.set(token, set);
|
|
261
|
+
}
|
|
262
|
+
set.add(nodeId);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
getStats() {
|
|
266
|
+
const breakdown = {};
|
|
267
|
+
for (const [col, ids] of this.collectionDocIds) {
|
|
268
|
+
breakdown[col] = ids.size;
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
collectionsCount: this.collectionDocIds.size,
|
|
272
|
+
totalDocuments: this.documents.size,
|
|
273
|
+
totalSynapses: this.edgeMeta.size,
|
|
274
|
+
collectionBreakdown: breakdown
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// serializer.ts
|
|
280
|
+
import { existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
281
|
+
import { dirname } from "path";
|
|
282
|
+
function extractWikilinks(text, defaultCollection = "") {
|
|
283
|
+
const regex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
284
|
+
const links = [];
|
|
285
|
+
let match;
|
|
286
|
+
while ((match = regex.exec(text)) !== null) {
|
|
287
|
+
const rawTarget = match[1].trim();
|
|
288
|
+
if (!rawTarget)
|
|
289
|
+
continue;
|
|
290
|
+
const alias = match[2]?.trim();
|
|
291
|
+
let targetCollection = defaultCollection;
|
|
292
|
+
let targetId = rawTarget;
|
|
293
|
+
if (rawTarget.includes("/")) {
|
|
294
|
+
const parts = rawTarget.split("/");
|
|
295
|
+
targetCollection = parts[0].trim();
|
|
296
|
+
targetId = parts.slice(1).join("/").trim();
|
|
297
|
+
}
|
|
298
|
+
if (!targetId)
|
|
299
|
+
continue;
|
|
300
|
+
links.push({
|
|
301
|
+
raw: match[0],
|
|
302
|
+
targetCollection,
|
|
303
|
+
targetId,
|
|
304
|
+
alias
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return links;
|
|
308
|
+
}
|
|
309
|
+
function parseMarkdownFile(rawContent, defaultCollection = "") {
|
|
310
|
+
const normalizedRaw = rawContent.replace(/\r\n/g, `
|
|
311
|
+
`);
|
|
312
|
+
const trimmed = normalizedRaw.trimStart();
|
|
313
|
+
if (!trimmed.startsWith(`---
|
|
314
|
+
`) && trimmed !== "---") {
|
|
315
|
+
const wikilinks = extractWikilinks(normalizedRaw, defaultCollection);
|
|
316
|
+
return { frontmatter: {}, body: normalizedRaw, wikilinks };
|
|
317
|
+
}
|
|
318
|
+
const endIdx = trimmed.indexOf(`
|
|
319
|
+
---`, 3);
|
|
320
|
+
if (endIdx === -1) {
|
|
321
|
+
const wikilinks = extractWikilinks(normalizedRaw, defaultCollection);
|
|
322
|
+
return { frontmatter: {}, body: normalizedRaw, wikilinks };
|
|
323
|
+
}
|
|
324
|
+
const yamlBlock = trimmed.slice(4, endIdx);
|
|
325
|
+
const afterDelimiter = trimmed.slice(endIdx + 4);
|
|
326
|
+
const body = afterDelimiter.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
327
|
+
const data = {};
|
|
328
|
+
const lines = yamlBlock.split(`
|
|
329
|
+
`);
|
|
330
|
+
let currentKey = "";
|
|
331
|
+
let inArray = false;
|
|
332
|
+
let arrayItems = [];
|
|
333
|
+
let inObjectArray = false;
|
|
334
|
+
let currentObj = null;
|
|
335
|
+
for (let line of lines) {
|
|
336
|
+
line = line.replace(/\r$/, "");
|
|
337
|
+
if (!line.trim() || line.trim().startsWith("#"))
|
|
338
|
+
continue;
|
|
339
|
+
if (inObjectArray && currentObj && /^\s{4,}[a-zA-Z0-9_-]+:/.test(line)) {
|
|
340
|
+
const match = line.match(/^\s+([a-zA-Z0-9_-]+):\s*(.*)$/);
|
|
341
|
+
if (match) {
|
|
342
|
+
const subKey = match[1];
|
|
343
|
+
const subVal = parseYamlScalar(match[2].trim());
|
|
344
|
+
currentObj[subKey] = subVal;
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (inArray && /^\s*-\s+/.test(line)) {
|
|
349
|
+
const itemContent = line.replace(/^\s*-\s+/, "").trim();
|
|
350
|
+
const objMatch = !itemContent.startsWith('"') && !itemContent.startsWith("'") && !itemContent.startsWith("{") && !itemContent.startsWith("[") ? itemContent.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/) : null;
|
|
351
|
+
if (objMatch) {
|
|
352
|
+
inObjectArray = true;
|
|
353
|
+
currentObj = {};
|
|
354
|
+
const subKey = objMatch[1];
|
|
355
|
+
const subVal = parseYamlScalar(objMatch[2].trim());
|
|
356
|
+
currentObj[subKey] = subVal;
|
|
357
|
+
arrayItems.push(currentObj);
|
|
358
|
+
} else {
|
|
359
|
+
inObjectArray = false;
|
|
360
|
+
currentObj = null;
|
|
361
|
+
arrayItems.push(parseYamlScalar(itemContent));
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (inArray && currentKey) {
|
|
366
|
+
data[currentKey] = arrayItems;
|
|
367
|
+
inArray = false;
|
|
368
|
+
inObjectArray = false;
|
|
369
|
+
currentObj = null;
|
|
370
|
+
}
|
|
371
|
+
const kvMatch = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
|
|
372
|
+
if (kvMatch) {
|
|
373
|
+
currentKey = kvMatch[1];
|
|
374
|
+
const valStr = kvMatch[2].trim();
|
|
375
|
+
if (valStr === "") {
|
|
376
|
+
inArray = true;
|
|
377
|
+
arrayItems = [];
|
|
378
|
+
} else {
|
|
379
|
+
data[currentKey] = parseYamlScalar(valStr);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (inArray && currentKey) {
|
|
384
|
+
data[currentKey] = arrayItems;
|
|
385
|
+
}
|
|
386
|
+
const wikilinks = extractWikilinks(normalizedRaw, defaultCollection);
|
|
387
|
+
return {
|
|
388
|
+
frontmatter: data,
|
|
389
|
+
body,
|
|
390
|
+
wikilinks
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function parseYamlScalar(val) {
|
|
394
|
+
if (val === "true")
|
|
395
|
+
return true;
|
|
396
|
+
if (val === "false")
|
|
397
|
+
return false;
|
|
398
|
+
if (val === "null" || val === "~" || val === "")
|
|
399
|
+
return null;
|
|
400
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("[") && val.endsWith("]") || val.startsWith("{") && val.endsWith("}")) {
|
|
401
|
+
try {
|
|
402
|
+
return JSON.parse(val);
|
|
403
|
+
} catch {
|
|
404
|
+
if (val.startsWith('"') && val.endsWith('"')) {
|
|
405
|
+
return val.slice(1, -1);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (val.startsWith("'") && val.endsWith("'")) {
|
|
410
|
+
return val.slice(1, -1);
|
|
411
|
+
}
|
|
412
|
+
if (/^-?\d+$/.test(val)) {
|
|
413
|
+
const num = Number(val);
|
|
414
|
+
if (Number.isSafeInteger(num)) {
|
|
415
|
+
return num;
|
|
416
|
+
}
|
|
417
|
+
return val;
|
|
418
|
+
}
|
|
419
|
+
if (/^-?\d+\.\d+$/.test(val)) {
|
|
420
|
+
const num = Number(val);
|
|
421
|
+
if (Number.isFinite(num)) {
|
|
422
|
+
return num;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return val;
|
|
426
|
+
}
|
|
427
|
+
function serializeYamlValue(val, indent = 0) {
|
|
428
|
+
const pad = " ".repeat(indent);
|
|
429
|
+
if (val === null || val === undefined)
|
|
430
|
+
return "null";
|
|
431
|
+
if (typeof val === "boolean")
|
|
432
|
+
return String(val);
|
|
433
|
+
if (typeof val === "number")
|
|
434
|
+
return Number.isFinite(val) ? String(val) : "null";
|
|
435
|
+
if (typeof val === "string") {
|
|
436
|
+
return JSON.stringify(val);
|
|
437
|
+
}
|
|
438
|
+
if (Array.isArray(val)) {
|
|
439
|
+
if (val.length === 0)
|
|
440
|
+
return "[]";
|
|
441
|
+
if (indent > 0) {
|
|
442
|
+
return JSON.stringify(val);
|
|
443
|
+
}
|
|
444
|
+
const lines = [""];
|
|
445
|
+
for (const item of val) {
|
|
446
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
447
|
+
const entries = Object.entries(item).filter(([, v]) => v !== undefined);
|
|
448
|
+
if (entries.length === 0) {
|
|
449
|
+
lines.push(`${pad} - {}`);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const [firstK, firstV] = entries[0];
|
|
453
|
+
lines.push(`${pad} - ${firstK}: ${serializeYamlValue(firstV, indent + 4)}`);
|
|
454
|
+
for (let i = 1;i < entries.length; i++) {
|
|
455
|
+
const [k, v] = entries[i];
|
|
456
|
+
lines.push(`${pad} ${k}: ${serializeYamlValue(v, indent + 4)}`);
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
lines.push(`${pad} - ${serializeYamlValue(item, indent + 2)}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return lines.join(`
|
|
463
|
+
`);
|
|
464
|
+
}
|
|
465
|
+
return JSON.stringify(val);
|
|
466
|
+
}
|
|
467
|
+
function stringifyDocument(data, body = "") {
|
|
468
|
+
const lines = ["---"];
|
|
469
|
+
for (const [key, value] of Object.entries(data)) {
|
|
470
|
+
if (value === undefined)
|
|
471
|
+
continue;
|
|
472
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
473
|
+
lines.push(`${key}:${serializeYamlValue(value, 0)}`);
|
|
474
|
+
} else {
|
|
475
|
+
lines.push(`${key}: ${serializeYamlValue(value, 0)}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
lines.push("---");
|
|
479
|
+
lines.push("");
|
|
480
|
+
const trimmedBody = body.replace(/\r\n/g, `
|
|
481
|
+
`).replace(/^\n+/, "").replace(/\n+$/, "");
|
|
482
|
+
if (trimmedBody.length > 0) {
|
|
483
|
+
lines.push(trimmedBody);
|
|
484
|
+
lines.push("");
|
|
485
|
+
}
|
|
486
|
+
return lines.join(`
|
|
487
|
+
`);
|
|
488
|
+
}
|
|
489
|
+
function atomicWriteFile(targetPath, content) {
|
|
490
|
+
const dir = dirname(targetPath);
|
|
491
|
+
if (!existsSync(dir)) {
|
|
492
|
+
mkdirSync(dir, { recursive: true });
|
|
493
|
+
}
|
|
494
|
+
const tmpPath = `${targetPath}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
495
|
+
try {
|
|
496
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
497
|
+
renameSync(tmpPath, targetPath);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
try {
|
|
500
|
+
if (existsSync(tmpPath)) {
|
|
501
|
+
unlinkSync(tmpPath);
|
|
502
|
+
}
|
|
503
|
+
} catch {}
|
|
504
|
+
throw err;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// engine.ts
|
|
509
|
+
class SynapseEngine {
|
|
510
|
+
vaultDir;
|
|
511
|
+
connectome;
|
|
512
|
+
config;
|
|
513
|
+
isInitialized = false;
|
|
514
|
+
pendingWriteQueue = new Map;
|
|
515
|
+
writeTimer = null;
|
|
516
|
+
watcherDebounceTimer = null;
|
|
517
|
+
internalWriteResetTimer = null;
|
|
518
|
+
fileWatcher = null;
|
|
519
|
+
isWritingInternally = false;
|
|
520
|
+
constructor(config) {
|
|
521
|
+
this.config = {
|
|
522
|
+
autoWatch: true,
|
|
523
|
+
persistDebounceMs: 100,
|
|
524
|
+
...config,
|
|
525
|
+
vaultDir: resolve(config.vaultDir)
|
|
526
|
+
};
|
|
527
|
+
this.vaultDir = this.config.vaultDir;
|
|
528
|
+
this.connectome = new SynapseConnectome;
|
|
529
|
+
}
|
|
530
|
+
async init() {
|
|
531
|
+
if (this.isInitialized)
|
|
532
|
+
return;
|
|
533
|
+
if (!existsSync2(this.vaultDir)) {
|
|
534
|
+
mkdirSync2(this.vaultDir, { recursive: true });
|
|
535
|
+
}
|
|
536
|
+
this.reloadVault();
|
|
537
|
+
if (this.config.autoWatch) {
|
|
538
|
+
this.startWatcher();
|
|
539
|
+
}
|
|
540
|
+
this.isInitialized = true;
|
|
541
|
+
}
|
|
542
|
+
reloadVault() {
|
|
543
|
+
this.connectome.clear();
|
|
544
|
+
if (!existsSync2(this.vaultDir))
|
|
545
|
+
return;
|
|
546
|
+
const entries = readdirSync(this.vaultDir, { withFileTypes: true });
|
|
547
|
+
for (const entry of entries) {
|
|
548
|
+
if (entry.isDirectory()) {
|
|
549
|
+
const collectionName = entry.name;
|
|
550
|
+
const colPath = join(this.vaultDir, collectionName);
|
|
551
|
+
this.loadCollectionDir(collectionName, colPath);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
loadCollectionDir(collectionName, dirPath) {
|
|
556
|
+
try {
|
|
557
|
+
const files = readdirSync(dirPath);
|
|
558
|
+
for (const file of files) {
|
|
559
|
+
if (!file.endsWith(".md") || file.includes(".tmp"))
|
|
560
|
+
continue;
|
|
561
|
+
const id = basename(file, ".md");
|
|
562
|
+
const fullPath = join(dirPath, file);
|
|
563
|
+
try {
|
|
564
|
+
const raw = readFileSync2(fullPath, "utf-8");
|
|
565
|
+
const parsed = parseMarkdownFile(raw, collectionName);
|
|
566
|
+
const rawFm = { ...parsed.frontmatter };
|
|
567
|
+
const createdAt = typeof rawFm._createdAt === "string" ? rawFm._createdAt : typeof rawFm.createdAt === "string" ? rawFm.createdAt : new Date().toISOString();
|
|
568
|
+
const updatedAt = typeof rawFm._updatedAt === "string" ? rawFm._updatedAt : typeof rawFm.updatedAt === "string" ? rawFm.updatedAt : new Date().toISOString();
|
|
569
|
+
delete rawFm._createdAt;
|
|
570
|
+
delete rawFm._updatedAt;
|
|
571
|
+
const doc = {
|
|
572
|
+
id,
|
|
573
|
+
collection: collectionName,
|
|
574
|
+
data: rawFm,
|
|
575
|
+
body: parsed.body,
|
|
576
|
+
createdAt,
|
|
577
|
+
updatedAt,
|
|
578
|
+
wikilinks: parsed.wikilinks
|
|
579
|
+
};
|
|
580
|
+
this.connectome.putDocument(doc);
|
|
581
|
+
} catch (err) {
|
|
582
|
+
process.stderr.write(`[SynapseEngine] \uD30C\uC77C \uB85C\uB4DC \uC2E4\uD328 (${file}): ${err?.message || err}
|
|
583
|
+
`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} catch {}
|
|
587
|
+
}
|
|
588
|
+
async saveDocument(collection, input) {
|
|
589
|
+
const now = new Date().toISOString();
|
|
590
|
+
const existing = this.connectome.getDocument(collection, input.id);
|
|
591
|
+
const createdAt = input.createdAt || existing?.createdAt || now;
|
|
592
|
+
const updatedAt = input.updatedAt || now;
|
|
593
|
+
const cleanBody = (input.body || "").replace(/\r\n/g, `
|
|
594
|
+
`).replace(/^\n+/, "").replace(/\n+$/, "");
|
|
595
|
+
const rawMarkdown = stringifyDocument({
|
|
596
|
+
...input.data,
|
|
597
|
+
_createdAt: createdAt,
|
|
598
|
+
_updatedAt: updatedAt
|
|
599
|
+
}, cleanBody);
|
|
600
|
+
const parsed = parseMarkdownFile(rawMarkdown, collection);
|
|
601
|
+
const restoredFm = { ...parsed.frontmatter };
|
|
602
|
+
delete restoredFm._createdAt;
|
|
603
|
+
delete restoredFm._updatedAt;
|
|
604
|
+
const finalDoc = {
|
|
605
|
+
id: input.id,
|
|
606
|
+
collection,
|
|
607
|
+
data: restoredFm,
|
|
608
|
+
body: parsed.body,
|
|
609
|
+
createdAt,
|
|
610
|
+
updatedAt,
|
|
611
|
+
wikilinks: parsed.wikilinks
|
|
612
|
+
};
|
|
613
|
+
this.connectome.putDocument(finalDoc);
|
|
614
|
+
const safeCol = collection.replace(/[\/\\:*?"<>|]/g, "_");
|
|
615
|
+
const safeId = input.id.replace(/[\/\\:*?"<>|]/g, "_");
|
|
616
|
+
const targetDir = join(this.vaultDir, safeCol);
|
|
617
|
+
const targetPath = join(targetDir, `${safeId}.md`);
|
|
618
|
+
const writeKey = `${collection}/${input.id}`;
|
|
619
|
+
this.scheduleWrite(writeKey, () => {
|
|
620
|
+
this.markInternalWrite();
|
|
621
|
+
atomicWriteFile(targetPath, rawMarkdown);
|
|
622
|
+
});
|
|
623
|
+
return finalDoc;
|
|
624
|
+
}
|
|
625
|
+
markInternalWrite() {
|
|
626
|
+
this.isWritingInternally = true;
|
|
627
|
+
if (this.internalWriteResetTimer) {
|
|
628
|
+
clearTimeout(this.internalWriteResetTimer);
|
|
629
|
+
}
|
|
630
|
+
this.internalWriteResetTimer = setTimeout(() => {
|
|
631
|
+
this.isWritingInternally = false;
|
|
632
|
+
this.internalWriteResetTimer = null;
|
|
633
|
+
}, 120);
|
|
634
|
+
}
|
|
635
|
+
async deleteDocument(collection, id) {
|
|
636
|
+
const deleted = this.connectome.removeDocument(collection, id);
|
|
637
|
+
if (!deleted)
|
|
638
|
+
return false;
|
|
639
|
+
const safeCol = collection.replace(/[\/\\:*?"<>|]/g, "_");
|
|
640
|
+
const safeId = id.replace(/[\/\\:*?"<>|]/g, "_");
|
|
641
|
+
const targetPath = join(this.vaultDir, safeCol, `${safeId}.md`);
|
|
642
|
+
const writeKey = `${collection}/${id}`;
|
|
643
|
+
this.pendingWriteQueue.delete(writeKey);
|
|
644
|
+
try {
|
|
645
|
+
if (existsSync2(targetPath)) {
|
|
646
|
+
this.markInternalWrite();
|
|
647
|
+
unlinkSync2(targetPath);
|
|
648
|
+
}
|
|
649
|
+
} catch {}
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
scheduleWrite(key, fn) {
|
|
653
|
+
this.pendingWriteQueue.set(key, fn);
|
|
654
|
+
if (this.writeTimer)
|
|
655
|
+
clearTimeout(this.writeTimer);
|
|
656
|
+
const debounceMs = this.config.persistDebounceMs || 100;
|
|
657
|
+
this.writeTimer = setTimeout(() => {
|
|
658
|
+
this.flushPendingWrites();
|
|
659
|
+
}, debounceMs);
|
|
660
|
+
}
|
|
661
|
+
flushPendingWrites() {
|
|
662
|
+
if (this.writeTimer) {
|
|
663
|
+
clearTimeout(this.writeTimer);
|
|
664
|
+
this.writeTimer = null;
|
|
665
|
+
}
|
|
666
|
+
if (this.pendingWriteQueue.size === 0)
|
|
667
|
+
return;
|
|
668
|
+
for (const [key, fn] of this.pendingWriteQueue) {
|
|
669
|
+
try {
|
|
670
|
+
fn();
|
|
671
|
+
} catch (err) {
|
|
672
|
+
process.stderr.write(`[SynapseEngine] \uB514\uC2A4\uD06C \uC4F0\uAE30 \uC2E4\uD328 (${key}): ${err?.message || err}
|
|
673
|
+
`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
this.pendingWriteQueue.clear();
|
|
677
|
+
}
|
|
678
|
+
startWatcher() {
|
|
679
|
+
try {
|
|
680
|
+
this.fileWatcher = watch(this.vaultDir, { recursive: true }, (_eventType, filename) => {
|
|
681
|
+
if (!filename || !filename.endsWith(".md") || filename.includes(".tmp"))
|
|
682
|
+
return;
|
|
683
|
+
if (this.isWritingInternally)
|
|
684
|
+
return;
|
|
685
|
+
if (this.watcherDebounceTimer) {
|
|
686
|
+
clearTimeout(this.watcherDebounceTimer);
|
|
687
|
+
}
|
|
688
|
+
this.watcherDebounceTimer = setTimeout(() => {
|
|
689
|
+
this.watcherDebounceTimer = null;
|
|
690
|
+
if (!this.isWritingInternally) {
|
|
691
|
+
this.reloadVault();
|
|
692
|
+
}
|
|
693
|
+
}, 80);
|
|
694
|
+
});
|
|
695
|
+
} catch {}
|
|
696
|
+
}
|
|
697
|
+
getStats() {
|
|
698
|
+
return this.connectome.getStats();
|
|
699
|
+
}
|
|
700
|
+
close() {
|
|
701
|
+
if (this.writeTimer) {
|
|
702
|
+
clearTimeout(this.writeTimer);
|
|
703
|
+
this.writeTimer = null;
|
|
704
|
+
}
|
|
705
|
+
if (this.watcherDebounceTimer) {
|
|
706
|
+
clearTimeout(this.watcherDebounceTimer);
|
|
707
|
+
this.watcherDebounceTimer = null;
|
|
708
|
+
}
|
|
709
|
+
if (this.internalWriteResetTimer) {
|
|
710
|
+
clearTimeout(this.internalWriteResetTimer);
|
|
711
|
+
this.internalWriteResetTimer = null;
|
|
712
|
+
}
|
|
713
|
+
this.flushPendingWrites();
|
|
714
|
+
if (this.fileWatcher) {
|
|
715
|
+
this.fileWatcher.close();
|
|
716
|
+
this.fileWatcher = null;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// collection.ts
|
|
722
|
+
class SynapseCollection {
|
|
723
|
+
name;
|
|
724
|
+
engine;
|
|
725
|
+
constructor(name, engine) {
|
|
726
|
+
this.name = name;
|
|
727
|
+
this.engine = engine;
|
|
728
|
+
}
|
|
729
|
+
async save(input) {
|
|
730
|
+
return this.engine.saveDocument(this.name, input);
|
|
731
|
+
}
|
|
732
|
+
get(id) {
|
|
733
|
+
return this.engine.connectome.getDocument(this.name, id);
|
|
734
|
+
}
|
|
735
|
+
has(id) {
|
|
736
|
+
return this.engine.connectome.getDocument(this.name, id) !== null;
|
|
737
|
+
}
|
|
738
|
+
async delete(id) {
|
|
739
|
+
return this.engine.deleteDocument(this.name, id);
|
|
740
|
+
}
|
|
741
|
+
query(options) {
|
|
742
|
+
return this.engine.connectome.queryCollection(this.name, options);
|
|
743
|
+
}
|
|
744
|
+
all() {
|
|
745
|
+
return this.engine.connectome.queryCollection(this.name);
|
|
746
|
+
}
|
|
747
|
+
count() {
|
|
748
|
+
return this.engine.connectome.queryCollection(this.name).length;
|
|
749
|
+
}
|
|
750
|
+
search(queryText) {
|
|
751
|
+
return this.engine.connectome.search(queryText, this.name);
|
|
752
|
+
}
|
|
753
|
+
synapses(id) {
|
|
754
|
+
const engine = this.engine;
|
|
755
|
+
const colName = this.name;
|
|
756
|
+
return {
|
|
757
|
+
outgoing: (targetCollectionFilter) => {
|
|
758
|
+
return engine.connectome.getOutgoingDocuments(colName, id, targetCollectionFilter);
|
|
759
|
+
},
|
|
760
|
+
incoming: (sourceCollectionFilter) => {
|
|
761
|
+
return engine.connectome.getIncomingDocuments(colName, id, sourceCollectionFilter);
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// migrator.ts
|
|
768
|
+
import { existsSync as existsSync3 } from "fs";
|
|
769
|
+
|
|
770
|
+
class SynapseMigrator {
|
|
771
|
+
engine;
|
|
772
|
+
constructor(engine) {
|
|
773
|
+
this.engine = engine;
|
|
774
|
+
}
|
|
775
|
+
async migrateFromSqlite(sqliteDbPath, mappings) {
|
|
776
|
+
if (!existsSync3(sqliteDbPath)) {
|
|
777
|
+
throw new Error(`[SynapseMigrator] SQLite DB \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${sqliteDbPath}`);
|
|
778
|
+
}
|
|
779
|
+
for (const m of mappings) {
|
|
780
|
+
if (!/^[a-zA-Z0-9_]+$/.test(m.tableName)) {
|
|
781
|
+
throw new Error(`[SynapseMigrator] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 SQLite \uD14C\uC774\uBE14 \uC2DD\uBCC4\uC790\uC785\uB2C8\uB2E4 (\uBCF4\uC548 \uCC28\uB2E8): ${m.tableName}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const { Database } = await import("bun:sqlite");
|
|
785
|
+
const sqlite = new Database(sqliteDbPath, { readonly: true });
|
|
786
|
+
const stats = {};
|
|
787
|
+
try {
|
|
788
|
+
for (const m of mappings) {
|
|
789
|
+
let count = 0;
|
|
790
|
+
try {
|
|
791
|
+
const rows = sqlite.query(`SELECT * FROM "${m.tableName}"`).all();
|
|
792
|
+
for (const row of rows) {
|
|
793
|
+
const mapped = m.mapRow(row);
|
|
794
|
+
await this.engine.saveDocument(m.targetCollection, {
|
|
795
|
+
id: mapped.id,
|
|
796
|
+
data: mapped.data,
|
|
797
|
+
body: mapped.body
|
|
798
|
+
});
|
|
799
|
+
count++;
|
|
800
|
+
}
|
|
801
|
+
} catch (err) {
|
|
802
|
+
process.stderr.write(`[SynapseMigrator] \uD14C\uC774\uBE14(${m.tableName}) \uBCC0\uD658 \uC911 \uC624\uB958: ${err?.message || err}
|
|
803
|
+
`);
|
|
804
|
+
}
|
|
805
|
+
stats[m.targetCollection] = count;
|
|
806
|
+
}
|
|
807
|
+
this.engine.flushPendingWrites();
|
|
808
|
+
sqlite.close();
|
|
809
|
+
return stats;
|
|
810
|
+
} catch (err) {
|
|
811
|
+
sqlite.close();
|
|
812
|
+
throw err;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async migrateFromJsonArray(targetCollection, items, mapItem) {
|
|
816
|
+
let count = 0;
|
|
817
|
+
for (const item of items) {
|
|
818
|
+
const input = mapItem(item);
|
|
819
|
+
await this.engine.saveDocument(targetCollection, input);
|
|
820
|
+
count++;
|
|
821
|
+
}
|
|
822
|
+
this.engine.flushPendingWrites();
|
|
823
|
+
return count;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// canvasExporter.ts
|
|
828
|
+
var COLLECTION_COLOR_PALETTE = {
|
|
829
|
+
guilds: "5",
|
|
830
|
+
streamers: "4",
|
|
831
|
+
clips: "1",
|
|
832
|
+
categories: "2",
|
|
833
|
+
settings: "6",
|
|
834
|
+
users: "3"
|
|
835
|
+
};
|
|
836
|
+
function exportToObsidianCanvas(connectome, targetFilePath, options) {
|
|
837
|
+
const nodeWidth = options?.nodeWidth || 280;
|
|
838
|
+
const nodeHeight = options?.nodeHeight || 160;
|
|
839
|
+
const hGap = options?.horizontalGap || 80;
|
|
840
|
+
const vGap = options?.verticalGap || 60;
|
|
841
|
+
const allDocs = connectome.getAllDocuments();
|
|
842
|
+
const allEdges = connectome.getAllEdges();
|
|
843
|
+
const groupedByCollection = new Map;
|
|
844
|
+
for (const doc of allDocs) {
|
|
845
|
+
let list = groupedByCollection.get(doc.collection);
|
|
846
|
+
if (!list) {
|
|
847
|
+
list = [];
|
|
848
|
+
groupedByCollection.set(doc.collection, list);
|
|
849
|
+
}
|
|
850
|
+
list.push(doc);
|
|
851
|
+
}
|
|
852
|
+
const nodes = [];
|
|
853
|
+
const nodeIdToCanvasId = new Map;
|
|
854
|
+
let colIndex = 0;
|
|
855
|
+
for (const [colName, docs] of groupedByCollection) {
|
|
856
|
+
const color = COLLECTION_COLOR_PALETTE[colName] || "5";
|
|
857
|
+
const x = colIndex * (nodeWidth + hGap);
|
|
858
|
+
docs.forEach((doc, rowIndex) => {
|
|
859
|
+
const internalId = `${doc.collection}/${doc.id}`;
|
|
860
|
+
const canvasNodeId = `node_${doc.collection}_${doc.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|
861
|
+
nodeIdToCanvasId.set(internalId, canvasNodeId);
|
|
862
|
+
const y = rowIndex * (nodeHeight + vGap);
|
|
863
|
+
nodes.push({
|
|
864
|
+
id: canvasNodeId,
|
|
865
|
+
type: "file",
|
|
866
|
+
file: `${doc.collection}/${doc.id}.md`,
|
|
867
|
+
x,
|
|
868
|
+
y,
|
|
869
|
+
width: nodeWidth,
|
|
870
|
+
height: nodeHeight,
|
|
871
|
+
color
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
colIndex++;
|
|
875
|
+
}
|
|
876
|
+
const edges = [];
|
|
877
|
+
let edgeCounter = 0;
|
|
878
|
+
for (const edge of allEdges) {
|
|
879
|
+
const fromCanvasId = nodeIdToCanvasId.get(edge.fromNodeId);
|
|
880
|
+
const toCanvasId = nodeIdToCanvasId.get(edge.toNodeId);
|
|
881
|
+
if (fromCanvasId && toCanvasId) {
|
|
882
|
+
edges.push({
|
|
883
|
+
id: `edge_${++edgeCounter}`,
|
|
884
|
+
fromNode: fromCanvasId,
|
|
885
|
+
toNode: toCanvasId,
|
|
886
|
+
label: edge.alias || edge.relation,
|
|
887
|
+
toEnd: "arrow"
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const canvasData = { nodes, edges };
|
|
892
|
+
atomicWriteFile(targetFilePath, JSON.stringify(canvasData, null, 2));
|
|
893
|
+
return canvasData;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// index.ts
|
|
897
|
+
class SynapseDB {
|
|
898
|
+
engine;
|
|
899
|
+
collections = new Map;
|
|
900
|
+
constructor(config) {
|
|
901
|
+
this.engine = new SynapseEngine(config);
|
|
902
|
+
}
|
|
903
|
+
async init() {
|
|
904
|
+
await this.engine.init();
|
|
905
|
+
}
|
|
906
|
+
collection(name) {
|
|
907
|
+
let col = this.collections.get(name);
|
|
908
|
+
if (!col) {
|
|
909
|
+
col = new SynapseCollection(name, this.engine);
|
|
910
|
+
this.collections.set(name, col);
|
|
911
|
+
}
|
|
912
|
+
return col;
|
|
913
|
+
}
|
|
914
|
+
exportCanvas(targetFilePath, options) {
|
|
915
|
+
return exportToObsidianCanvas(this.engine.connectome, targetFilePath, options);
|
|
916
|
+
}
|
|
917
|
+
createMigrator() {
|
|
918
|
+
return new SynapseMigrator(this.engine);
|
|
919
|
+
}
|
|
920
|
+
stats() {
|
|
921
|
+
return this.engine.getStats();
|
|
922
|
+
}
|
|
923
|
+
flush() {
|
|
924
|
+
this.engine.flushPendingWrites();
|
|
925
|
+
}
|
|
926
|
+
close() {
|
|
927
|
+
this.engine.close();
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
export {
|
|
931
|
+
SynapseCollection,
|
|
932
|
+
SynapseConnectome,
|
|
933
|
+
SynapseDB,
|
|
934
|
+
SynapseEngine,
|
|
935
|
+
SynapseMigrator,
|
|
936
|
+
atomicWriteFile,
|
|
937
|
+
exportToObsidianCanvas,
|
|
938
|
+
extractWikilinks,
|
|
939
|
+
parseMarkdownFile,
|
|
940
|
+
parseYamlScalar,
|
|
941
|
+
stringifyDocument
|
|
942
|
+
};
|