synapse-kit 0.0.0-stage → 0.3.1
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.ko.md +129 -0
- package/README.md +127 -2
- package/dist/causal-memory.json +4581 -0
- package/dist/cli-memory.d.ts +5 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3474 -0
- package/dist/compiler/causal-build.d.ts +51 -0
- package/dist/compiler/causal-graph.d.ts +6 -0
- package/dist/compiler/cli.d.ts +2 -0
- package/dist/compiler/cli.js +1299 -0
- package/dist/compiler/core/config.d.ts +16 -0
- package/dist/compiler/core/engine.d.ts +17 -0
- package/dist/compiler/core/reporter.d.ts +5 -0
- package/dist/compiler/i18n.d.ts +281 -0
- package/dist/compiler/index.d.ts +21 -0
- package/dist/compiler/index.js +1983 -0
- package/dist/compiler/plugins/cpp.d.ts +19 -0
- package/dist/compiler/plugins/csharp.d.ts +13 -0
- package/dist/compiler/plugins/go.d.ts +15 -0
- package/dist/compiler/plugins/java.d.ts +16 -0
- package/dist/compiler/plugins/native.d.ts +12 -0
- package/dist/compiler/plugins/nsis.d.ts +8 -0
- package/dist/compiler/plugins/python.d.ts +21 -0
- package/dist/compiler/plugins/rust.d.ts +9 -0
- package/dist/compiler/plugins/schema.d.ts +12 -0
- package/dist/compiler/plugins/svelte.d.ts +10 -0
- package/dist/compiler/plugins/typescript.d.ts +23 -0
- package/dist/compiler/types.d.ts +72 -0
- package/dist/compiler/virtual_120b_types.d.ts +18 -0
- package/dist/core/config.d.ts +16 -0
- package/dist/core/engine.d.ts +17 -0
- package/dist/core/reporter.d.ts +5 -0
- package/dist/db/index.js +1910 -0
- package/dist/i18n.d.ts +281 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8282 -0
- package/dist/memory/causal-benchmark.d.ts +29 -0
- package/dist/memory/causal-branch.d.ts +79 -0
- package/dist/memory/causal-build.d.ts +51 -0
- package/dist/memory/causal-config.d.ts +38 -0
- package/dist/memory/causal-context.d.ts +113 -0
- package/dist/memory/causal-contexts.json +316 -0
- package/dist/memory/causal-graph.d.ts +6 -0
- package/dist/memory/causal-graveyard.d.ts +94 -0
- package/dist/memory/causal-lens.d.ts +133 -0
- package/dist/memory/causal-memory.json +5054 -0
- package/dist/memory/causal-migrator.d.ts +11 -0
- package/dist/memory/causal-peer.d.ts +53 -0
- package/dist/memory/causal-predictor.d.ts +42 -0
- package/dist/memory/causal-query.d.ts +21 -0
- package/dist/memory/causal-server.d.ts +578 -0
- package/dist/memory/causal-store.d.ts +280 -0
- package/dist/memory/causal-synthesizer.d.ts +96 -0
- package/dist/memory/causal-timeseries.d.ts +92 -0
- package/dist/memory/cli.d.ts +13 -0
- package/dist/memory/cli.js +5668 -0
- package/dist/memory/index.d.ts +13 -0
- package/dist/memory/index.js +4451 -0
- package/dist/plugins/cpp.d.ts +19 -0
- package/dist/plugins/csharp.d.ts +13 -0
- package/dist/plugins/go.d.ts +15 -0
- package/dist/plugins/java.d.ts +16 -0
- package/dist/plugins/native.d.ts +12 -0
- package/dist/plugins/nsis.d.ts +8 -0
- package/dist/plugins/python.d.ts +21 -0
- package/dist/plugins/rust.d.ts +9 -0
- package/dist/plugins/schema.d.ts +12 -0
- package/dist/plugins/svelte.d.ts +10 -0
- package/dist/plugins/typescript.d.ts +23 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/core/config.d.ts +12 -0
- package/dist/src/core/engine.d.ts +9 -0
- package/dist/src/core/reporter.d.ts +5 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/memory/causal-graph.d.ts +6 -0
- package/dist/src/plugins/native.d.ts +12 -0
- package/dist/src/plugins/nsis.d.ts +8 -0
- package/dist/src/plugins/rust.d.ts +9 -0
- package/dist/src/plugins/schema.d.ts +12 -0
- package/dist/src/plugins/svelte.d.ts +10 -0
- package/dist/src/types.d.ts +55 -0
- package/dist/types.d.ts +72 -0
- package/package.json +87 -4
package/dist/db/index.js
ADDED
|
@@ -0,0 +1,1910 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// db/engine.ts
|
|
3
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync2, unlinkSync as unlinkSync2, watch } from "fs";
|
|
4
|
+
import { basename, join as join2, resolve as resolve2 } from "path";
|
|
5
|
+
|
|
6
|
+
// db/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
|
+
dirtySearchNodes = new Set;
|
|
16
|
+
constructor() {}
|
|
17
|
+
static makeNodeId(collection, id) {
|
|
18
|
+
return `${collection}/${id}`;
|
|
19
|
+
}
|
|
20
|
+
clear() {
|
|
21
|
+
this.documents.clear();
|
|
22
|
+
this.collectionDocIds.clear();
|
|
23
|
+
this.outgoingEdges.clear();
|
|
24
|
+
this.incomingEdges.clear();
|
|
25
|
+
this.edgeMeta.clear();
|
|
26
|
+
this.tokenInvertedIndex.clear();
|
|
27
|
+
this.documentTokens.clear();
|
|
28
|
+
this.dirtySearchNodes.clear();
|
|
29
|
+
}
|
|
30
|
+
putDocument(doc) {
|
|
31
|
+
const nodeId = SynapseConnectome.makeNodeId(doc.collection, doc.id);
|
|
32
|
+
const links = doc.wikilinks;
|
|
33
|
+
const linksLen = links.length;
|
|
34
|
+
const existingOutSet = this.outgoingEdges.get(nodeId);
|
|
35
|
+
let skipEdgeRebuild = false;
|
|
36
|
+
if (existingOutSet && existingOutSet.size === 1 && linksLen === 1) {
|
|
37
|
+
const link = links[0];
|
|
38
|
+
const targetNodeId = SynapseConnectome.makeNodeId(link.targetCollection || doc.collection, link.targetId);
|
|
39
|
+
if (existingOutSet.has(targetNodeId)) {
|
|
40
|
+
const metaKey = `${nodeId}->${targetNodeId}`;
|
|
41
|
+
const existingMeta = this.edgeMeta.get(metaKey);
|
|
42
|
+
if (existingMeta && existingMeta.alias === link.alias) {
|
|
43
|
+
skipEdgeRebuild = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!skipEdgeRebuild && existingOutSet) {
|
|
48
|
+
this.removeDocumentOutgoingEdges(nodeId);
|
|
49
|
+
}
|
|
50
|
+
this.documents.set(nodeId, doc);
|
|
51
|
+
let colSet = this.collectionDocIds.get(doc.collection);
|
|
52
|
+
if (!colSet) {
|
|
53
|
+
colSet = new Set;
|
|
54
|
+
this.collectionDocIds.set(doc.collection, colSet);
|
|
55
|
+
}
|
|
56
|
+
colSet.add(doc.id);
|
|
57
|
+
if (!skipEdgeRebuild && linksLen > 0) {
|
|
58
|
+
let outSet = this.outgoingEdges.get(nodeId);
|
|
59
|
+
if (!outSet) {
|
|
60
|
+
outSet = new Set;
|
|
61
|
+
this.outgoingEdges.set(nodeId, outSet);
|
|
62
|
+
}
|
|
63
|
+
for (let i = 0;i < linksLen; i++) {
|
|
64
|
+
const link = links[i];
|
|
65
|
+
const targetNodeId = SynapseConnectome.makeNodeId(link.targetCollection || doc.collection, link.targetId);
|
|
66
|
+
outSet.add(targetNodeId);
|
|
67
|
+
let inSet = this.incomingEdges.get(targetNodeId);
|
|
68
|
+
if (!inSet) {
|
|
69
|
+
inSet = new Set;
|
|
70
|
+
this.incomingEdges.set(targetNodeId, inSet);
|
|
71
|
+
}
|
|
72
|
+
inSet.add(nodeId);
|
|
73
|
+
this.edgeMeta.set(`${nodeId}->${targetNodeId}`, {
|
|
74
|
+
fromNodeId: nodeId,
|
|
75
|
+
toNodeId: targetNodeId,
|
|
76
|
+
alias: link.alias
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this.dirtySearchNodes.add(nodeId);
|
|
81
|
+
}
|
|
82
|
+
removeDocument(collection, id) {
|
|
83
|
+
const nodeId = SynapseConnectome.makeNodeId(collection, id);
|
|
84
|
+
if (!this.documents.has(nodeId))
|
|
85
|
+
return false;
|
|
86
|
+
this.dirtySearchNodes.delete(nodeId);
|
|
87
|
+
if (this.outgoingEdges.has(nodeId)) {
|
|
88
|
+
this.removeDocumentOutgoingEdges(nodeId);
|
|
89
|
+
}
|
|
90
|
+
this.removeDocumentSearchTokens(nodeId);
|
|
91
|
+
this.documents.delete(nodeId);
|
|
92
|
+
const colSet = this.collectionDocIds.get(collection);
|
|
93
|
+
if (colSet) {
|
|
94
|
+
colSet.delete(id);
|
|
95
|
+
if (colSet.size === 0) {
|
|
96
|
+
this.collectionDocIds.delete(collection);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
removeDocumentOutgoingEdges(nodeId) {
|
|
102
|
+
const outSet = this.outgoingEdges.get(nodeId);
|
|
103
|
+
if (!outSet)
|
|
104
|
+
return;
|
|
105
|
+
for (const targetNodeId of outSet) {
|
|
106
|
+
const inSet = this.incomingEdges.get(targetNodeId);
|
|
107
|
+
if (inSet) {
|
|
108
|
+
inSet.delete(nodeId);
|
|
109
|
+
if (inSet.size === 0) {
|
|
110
|
+
this.incomingEdges.delete(targetNodeId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
this.edgeMeta.delete(`${nodeId}->${targetNodeId}`);
|
|
114
|
+
}
|
|
115
|
+
this.outgoingEdges.delete(nodeId);
|
|
116
|
+
}
|
|
117
|
+
removeDocumentSearchTokens(nodeId) {
|
|
118
|
+
const oldTokens = this.documentTokens.get(nodeId);
|
|
119
|
+
if (!oldTokens)
|
|
120
|
+
return;
|
|
121
|
+
for (const token of oldTokens) {
|
|
122
|
+
const nodeSet = this.tokenInvertedIndex.get(token);
|
|
123
|
+
if (nodeSet) {
|
|
124
|
+
nodeSet.delete(nodeId);
|
|
125
|
+
if (nodeSet.size === 0) {
|
|
126
|
+
this.tokenInvertedIndex.delete(token);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
this.documentTokens.delete(nodeId);
|
|
131
|
+
}
|
|
132
|
+
flushDirtySearchIndex() {
|
|
133
|
+
if (this.dirtySearchNodes.size === 0)
|
|
134
|
+
return;
|
|
135
|
+
for (const nodeId of this.dirtySearchNodes) {
|
|
136
|
+
this.removeDocumentSearchTokens(nodeId);
|
|
137
|
+
const doc = this.documents.get(nodeId);
|
|
138
|
+
if (doc) {
|
|
139
|
+
this.indexDocumentText(nodeId, doc);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
this.dirtySearchNodes.clear();
|
|
143
|
+
}
|
|
144
|
+
getDocument(collection, id) {
|
|
145
|
+
const nodeId = SynapseConnectome.makeNodeId(collection, id);
|
|
146
|
+
return this.documents.get(nodeId) || null;
|
|
147
|
+
}
|
|
148
|
+
getOutgoingDocuments(collection, id, targetCollectionFilter) {
|
|
149
|
+
const nodeId = SynapseConnectome.makeNodeId(collection, id);
|
|
150
|
+
const targetNodeIds = this.outgoingEdges.get(nodeId);
|
|
151
|
+
if (!targetNodeIds)
|
|
152
|
+
return [];
|
|
153
|
+
const results = [];
|
|
154
|
+
for (const tNodeId of targetNodeIds) {
|
|
155
|
+
if (targetCollectionFilter && !tNodeId.startsWith(`${targetCollectionFilter}/`)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const doc = this.documents.get(tNodeId);
|
|
159
|
+
if (doc)
|
|
160
|
+
results.push(doc);
|
|
161
|
+
}
|
|
162
|
+
return results;
|
|
163
|
+
}
|
|
164
|
+
getIncomingDocuments(collection, id, sourceCollectionFilter) {
|
|
165
|
+
const nodeId = SynapseConnectome.makeNodeId(collection, id);
|
|
166
|
+
const sourceNodeIds = this.incomingEdges.get(nodeId);
|
|
167
|
+
if (!sourceNodeIds)
|
|
168
|
+
return [];
|
|
169
|
+
const results = [];
|
|
170
|
+
for (const sNodeId of sourceNodeIds) {
|
|
171
|
+
if (sourceCollectionFilter && !sNodeId.startsWith(`${sourceCollectionFilter}/`)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const doc = this.documents.get(sNodeId);
|
|
175
|
+
if (doc)
|
|
176
|
+
results.push(doc);
|
|
177
|
+
}
|
|
178
|
+
return results;
|
|
179
|
+
}
|
|
180
|
+
verifyGraphInvariants() {
|
|
181
|
+
this.flushDirtySearchIndex();
|
|
182
|
+
const brokenOutgoing = [];
|
|
183
|
+
const brokenIncoming = [];
|
|
184
|
+
const ghostTokens = [];
|
|
185
|
+
for (const [u, outTargets] of this.outgoingEdges) {
|
|
186
|
+
for (const v of outTargets) {
|
|
187
|
+
const inSources = this.incomingEdges.get(v);
|
|
188
|
+
if (!inSources || !inSources.has(u)) {
|
|
189
|
+
brokenOutgoing.push(`${u}->${v}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const [v, inSources] of this.incomingEdges) {
|
|
194
|
+
for (const u of inSources) {
|
|
195
|
+
const outTargets = this.outgoingEdges.get(u);
|
|
196
|
+
if (!outTargets || !outTargets.has(v)) {
|
|
197
|
+
brokenIncoming.push(`${u}->${v}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const [token, nodeSet] of this.tokenInvertedIndex) {
|
|
202
|
+
for (const nodeId of nodeSet) {
|
|
203
|
+
if (!this.documents.has(nodeId)) {
|
|
204
|
+
ghostTokens.push(`${token}@${nodeId}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
ok: brokenOutgoing.length === 0 && brokenIncoming.length === 0 && ghostTokens.length === 0,
|
|
210
|
+
brokenOutgoing,
|
|
211
|
+
brokenIncoming,
|
|
212
|
+
ghostTokens
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
getAllEdges() {
|
|
216
|
+
return Array.from(this.edgeMeta.values());
|
|
217
|
+
}
|
|
218
|
+
getAllDocuments() {
|
|
219
|
+
return Array.from(this.documents.values());
|
|
220
|
+
}
|
|
221
|
+
queryCollection(collection, options) {
|
|
222
|
+
const ids = this.collectionDocIds.get(collection);
|
|
223
|
+
if (!ids)
|
|
224
|
+
return [];
|
|
225
|
+
const docs = [];
|
|
226
|
+
for (const id of ids) {
|
|
227
|
+
const doc = this.documents.get(SynapseConnectome.makeNodeId(collection, id));
|
|
228
|
+
if (!doc)
|
|
229
|
+
continue;
|
|
230
|
+
if (options?.filter && !options.filter(doc))
|
|
231
|
+
continue;
|
|
232
|
+
docs.push(doc);
|
|
233
|
+
}
|
|
234
|
+
if (options?.sortBy) {
|
|
235
|
+
docs.sort(options.sortBy);
|
|
236
|
+
}
|
|
237
|
+
const offset = options?.offset || 0;
|
|
238
|
+
const limit = options?.limit !== undefined ? options.limit : docs.length;
|
|
239
|
+
return docs.slice(offset, offset + limit);
|
|
240
|
+
}
|
|
241
|
+
search(query, collectionFilter) {
|
|
242
|
+
this.flushDirtySearchIndex();
|
|
243
|
+
const tokens = this.tokenize(query);
|
|
244
|
+
if (tokens.length === 0)
|
|
245
|
+
return [];
|
|
246
|
+
const scoreMap = new Map;
|
|
247
|
+
for (const token of tokens) {
|
|
248
|
+
const matchedNodeIds = this.tokenInvertedIndex.get(token);
|
|
249
|
+
if (!matchedNodeIds)
|
|
250
|
+
continue;
|
|
251
|
+
for (const nodeId of matchedNodeIds) {
|
|
252
|
+
if (collectionFilter && !nodeId.startsWith(`${collectionFilter}/`)) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
let entry = scoreMap.get(nodeId);
|
|
256
|
+
if (!entry) {
|
|
257
|
+
entry = { score: 0, matchedTerms: new Set };
|
|
258
|
+
scoreMap.set(nodeId, entry);
|
|
259
|
+
}
|
|
260
|
+
entry.score += 1;
|
|
261
|
+
entry.matchedTerms.add(token);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const results = [];
|
|
265
|
+
for (const [nodeId, { score, matchedTerms }] of scoreMap) {
|
|
266
|
+
const doc = this.documents.get(nodeId);
|
|
267
|
+
if (!doc)
|
|
268
|
+
continue;
|
|
269
|
+
results.push({
|
|
270
|
+
document: doc,
|
|
271
|
+
score,
|
|
272
|
+
matchedTerms: Array.from(matchedTerms)
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return results.sort((a, b) => b.score - a.score);
|
|
276
|
+
}
|
|
277
|
+
tokenize(text) {
|
|
278
|
+
const normalized = text.toLowerCase().replace(/[\r\n\t]/g, " ");
|
|
279
|
+
const words = normalized.split(/[^a-zA-Z0-9\uAC00-\uD7A3]+/).filter((w) => w.length > 0);
|
|
280
|
+
const tokens = new Set;
|
|
281
|
+
for (const word of words) {
|
|
282
|
+
tokens.add(word);
|
|
283
|
+
if (/[\uAC00-\uD7A3]/.test(word) && word.length >= 2) {
|
|
284
|
+
for (let i = 0;i < word.length - 1; i++) {
|
|
285
|
+
tokens.add(word.slice(i, i + 2));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return Array.from(tokens);
|
|
290
|
+
}
|
|
291
|
+
indexDocumentText(nodeId, doc) {
|
|
292
|
+
const rawDataStr = JSON.stringify(doc.data);
|
|
293
|
+
const fullText = `${doc.id} ${doc.collection} ${rawDataStr} ${doc.body}`;
|
|
294
|
+
const tokens = this.tokenize(fullText);
|
|
295
|
+
const docTokenSet = new Set(tokens);
|
|
296
|
+
this.documentTokens.set(nodeId, docTokenSet);
|
|
297
|
+
for (const token of docTokenSet) {
|
|
298
|
+
let set = this.tokenInvertedIndex.get(token);
|
|
299
|
+
if (!set) {
|
|
300
|
+
set = new Set;
|
|
301
|
+
this.tokenInvertedIndex.set(token, set);
|
|
302
|
+
}
|
|
303
|
+
set.add(nodeId);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
getStats() {
|
|
307
|
+
const breakdown = {};
|
|
308
|
+
for (const [col, ids] of this.collectionDocIds) {
|
|
309
|
+
breakdown[col] = ids.size;
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
collectionsCount: this.collectionDocIds.size,
|
|
313
|
+
totalDocuments: this.documents.size,
|
|
314
|
+
totalSynapses: this.edgeMeta.size,
|
|
315
|
+
collectionBreakdown: breakdown
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// db/serializer.ts
|
|
321
|
+
import { existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
322
|
+
import { dirname } from "path";
|
|
323
|
+
var EMPTY_WIKILINKS = [];
|
|
324
|
+
function extractWikilinks(text, defaultCollection = "") {
|
|
325
|
+
if (!text)
|
|
326
|
+
return EMPTY_WIKILINKS;
|
|
327
|
+
let openIdx = text.indexOf("[[");
|
|
328
|
+
if (openIdx === -1)
|
|
329
|
+
return EMPTY_WIKILINKS;
|
|
330
|
+
const links = [];
|
|
331
|
+
const textLen = text.length;
|
|
332
|
+
while (openIdx !== -1 && openIdx + 4 <= textLen) {
|
|
333
|
+
const closeIdx = text.indexOf("]]", openIdx + 2);
|
|
334
|
+
if (closeIdx === -1)
|
|
335
|
+
break;
|
|
336
|
+
const inner = text.slice(openIdx + 2, closeIdx);
|
|
337
|
+
const nestedOpen = inner.indexOf("[[");
|
|
338
|
+
if (nestedOpen !== -1) {
|
|
339
|
+
openIdx = openIdx + 2 + nestedOpen;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
let rawTarget = inner;
|
|
343
|
+
let alias;
|
|
344
|
+
const pipeIdx = inner.indexOf("|");
|
|
345
|
+
if (pipeIdx !== -1) {
|
|
346
|
+
rawTarget = inner.slice(0, pipeIdx).trim();
|
|
347
|
+
const rawAlias = inner.slice(pipeIdx + 1).trim();
|
|
348
|
+
if (rawAlias.length > 0)
|
|
349
|
+
alias = rawAlias;
|
|
350
|
+
} else {
|
|
351
|
+
rawTarget = inner.trim();
|
|
352
|
+
}
|
|
353
|
+
if (rawTarget.length > 0) {
|
|
354
|
+
let targetCollection = defaultCollection;
|
|
355
|
+
let targetId = rawTarget;
|
|
356
|
+
const slashIdx = rawTarget.indexOf("/");
|
|
357
|
+
if (slashIdx !== -1) {
|
|
358
|
+
targetCollection = rawTarget.slice(0, slashIdx).trim();
|
|
359
|
+
targetId = rawTarget.slice(slashIdx + 1).trim();
|
|
360
|
+
}
|
|
361
|
+
if (targetId.length > 0) {
|
|
362
|
+
links.push({
|
|
363
|
+
raw: text.slice(openIdx, closeIdx + 2),
|
|
364
|
+
targetCollection,
|
|
365
|
+
targetId,
|
|
366
|
+
alias
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
openIdx = text.indexOf("[[", closeIdx + 2);
|
|
371
|
+
}
|
|
372
|
+
return links.length > 0 ? links : EMPTY_WIKILINKS;
|
|
373
|
+
}
|
|
374
|
+
function extractWikilinksFromDataAndBody(data, body, defaultCollection = "") {
|
|
375
|
+
let result = null;
|
|
376
|
+
if (data && typeof data === "object") {
|
|
377
|
+
for (const key in data) {
|
|
378
|
+
const val = data[key];
|
|
379
|
+
if (typeof val === "string" && val.includes("[[")) {
|
|
380
|
+
const extracted = extractWikilinks(val, defaultCollection);
|
|
381
|
+
if (extracted.length > 0) {
|
|
382
|
+
if (!result)
|
|
383
|
+
result = [];
|
|
384
|
+
for (let i = 0;i < extracted.length; i++)
|
|
385
|
+
result.push(extracted[i]);
|
|
386
|
+
}
|
|
387
|
+
} else if (typeof val === "object" && val !== null) {
|
|
388
|
+
const jsonStr = JSON.stringify(val);
|
|
389
|
+
if (jsonStr.includes("[[")) {
|
|
390
|
+
const extracted = extractWikilinks(jsonStr, defaultCollection);
|
|
391
|
+
if (extracted.length > 0) {
|
|
392
|
+
if (!result)
|
|
393
|
+
result = [];
|
|
394
|
+
for (let i = 0;i < extracted.length; i++)
|
|
395
|
+
result.push(extracted[i]);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (body && body.includes("[[")) {
|
|
402
|
+
const bodyLinks = extractWikilinks(body, defaultCollection);
|
|
403
|
+
if (bodyLinks.length > 0) {
|
|
404
|
+
if (!result)
|
|
405
|
+
return bodyLinks;
|
|
406
|
+
for (let i = 0;i < bodyLinks.length; i++)
|
|
407
|
+
result.push(bodyLinks[i]);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return result ?? EMPTY_WIKILINKS;
|
|
411
|
+
}
|
|
412
|
+
function parseMarkdownFile(rawContent, defaultCollection = "") {
|
|
413
|
+
const normalizedRaw = rawContent.replace(/\r\n/g, `
|
|
414
|
+
`);
|
|
415
|
+
const trimmed = normalizedRaw.trimStart();
|
|
416
|
+
if (!trimmed.startsWith(`---
|
|
417
|
+
`) && trimmed !== "---") {
|
|
418
|
+
const wikilinks = extractWikilinks(normalizedRaw, defaultCollection);
|
|
419
|
+
return { frontmatter: {}, body: normalizedRaw, wikilinks };
|
|
420
|
+
}
|
|
421
|
+
const endIdx = trimmed.indexOf(`
|
|
422
|
+
---`, 3);
|
|
423
|
+
if (endIdx === -1) {
|
|
424
|
+
const wikilinks = extractWikilinks(normalizedRaw, defaultCollection);
|
|
425
|
+
return { frontmatter: {}, body: normalizedRaw, wikilinks };
|
|
426
|
+
}
|
|
427
|
+
const yamlBlock = trimmed.slice(4, endIdx);
|
|
428
|
+
const afterDelimiter = trimmed.slice(endIdx + 4);
|
|
429
|
+
const body = afterDelimiter.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
430
|
+
const data = {};
|
|
431
|
+
const lines = yamlBlock.split(`
|
|
432
|
+
`);
|
|
433
|
+
let currentKey = "";
|
|
434
|
+
let inArray = false;
|
|
435
|
+
let arrayItems = [];
|
|
436
|
+
let inObjectArray = false;
|
|
437
|
+
let currentObj = null;
|
|
438
|
+
for (let line of lines) {
|
|
439
|
+
line = line.replace(/\r$/, "");
|
|
440
|
+
if (!line.trim() || line.trim().startsWith("#"))
|
|
441
|
+
continue;
|
|
442
|
+
if (inObjectArray && currentObj && /^\s{4,}[a-zA-Z0-9_-]+:/.test(line)) {
|
|
443
|
+
const match = line.match(/^\s+([a-zA-Z0-9_-]+):\s*(.*)$/);
|
|
444
|
+
if (match) {
|
|
445
|
+
const subKey = match[1];
|
|
446
|
+
const subVal = parseYamlScalar(match[2].trim());
|
|
447
|
+
currentObj[subKey] = subVal;
|
|
448
|
+
}
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (inArray && /^\s*-\s+/.test(line)) {
|
|
452
|
+
const itemContent = line.replace(/^\s*-\s+/, "").trim();
|
|
453
|
+
const objMatch = !itemContent.startsWith('"') && !itemContent.startsWith("'") && !itemContent.startsWith("{") && !itemContent.startsWith("[") ? itemContent.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/) : null;
|
|
454
|
+
if (objMatch) {
|
|
455
|
+
inObjectArray = true;
|
|
456
|
+
currentObj = {};
|
|
457
|
+
const subKey = objMatch[1];
|
|
458
|
+
const subVal = parseYamlScalar(objMatch[2].trim());
|
|
459
|
+
currentObj[subKey] = subVal;
|
|
460
|
+
arrayItems.push(currentObj);
|
|
461
|
+
} else {
|
|
462
|
+
inObjectArray = false;
|
|
463
|
+
currentObj = null;
|
|
464
|
+
arrayItems.push(parseYamlScalar(itemContent));
|
|
465
|
+
}
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (inArray && currentKey) {
|
|
469
|
+
data[currentKey] = arrayItems;
|
|
470
|
+
inArray = false;
|
|
471
|
+
inObjectArray = false;
|
|
472
|
+
currentObj = null;
|
|
473
|
+
}
|
|
474
|
+
const kvMatch = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
|
|
475
|
+
if (kvMatch) {
|
|
476
|
+
currentKey = kvMatch[1];
|
|
477
|
+
const valStr = kvMatch[2].trim();
|
|
478
|
+
if (valStr === "") {
|
|
479
|
+
inArray = true;
|
|
480
|
+
arrayItems = [];
|
|
481
|
+
} else {
|
|
482
|
+
data[currentKey] = parseYamlScalar(valStr);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (inArray && currentKey) {
|
|
487
|
+
data[currentKey] = arrayItems;
|
|
488
|
+
}
|
|
489
|
+
const wikilinks = extractWikilinks(normalizedRaw, defaultCollection);
|
|
490
|
+
return {
|
|
491
|
+
frontmatter: data,
|
|
492
|
+
body,
|
|
493
|
+
wikilinks
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function parseYamlScalar(val) {
|
|
497
|
+
if (val === "true")
|
|
498
|
+
return true;
|
|
499
|
+
if (val === "false")
|
|
500
|
+
return false;
|
|
501
|
+
if (val === "null" || val === "~" || val === "")
|
|
502
|
+
return null;
|
|
503
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("[") && val.endsWith("]") || val.startsWith("{") && val.endsWith("}")) {
|
|
504
|
+
try {
|
|
505
|
+
return JSON.parse(val);
|
|
506
|
+
} catch {
|
|
507
|
+
if (val.startsWith('"') && val.endsWith('"')) {
|
|
508
|
+
return val.slice(1, -1);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (val.startsWith("'") && val.endsWith("'")) {
|
|
513
|
+
return val.slice(1, -1);
|
|
514
|
+
}
|
|
515
|
+
if (/^-?\d+$/.test(val)) {
|
|
516
|
+
const num = Number(val);
|
|
517
|
+
if (Number.isSafeInteger(num)) {
|
|
518
|
+
return num;
|
|
519
|
+
}
|
|
520
|
+
return val;
|
|
521
|
+
}
|
|
522
|
+
if (/^-?\d+\.\d+$/.test(val)) {
|
|
523
|
+
const num = Number(val);
|
|
524
|
+
if (Number.isFinite(num)) {
|
|
525
|
+
return num;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return val;
|
|
529
|
+
}
|
|
530
|
+
function serializeYamlValue(val, indent = 0) {
|
|
531
|
+
const pad = " ".repeat(indent);
|
|
532
|
+
if (val === null || val === undefined)
|
|
533
|
+
return "null";
|
|
534
|
+
if (typeof val === "boolean")
|
|
535
|
+
return String(val);
|
|
536
|
+
if (typeof val === "number")
|
|
537
|
+
return Number.isFinite(val) ? String(val) : "null";
|
|
538
|
+
if (typeof val === "string") {
|
|
539
|
+
return JSON.stringify(val);
|
|
540
|
+
}
|
|
541
|
+
if (Array.isArray(val)) {
|
|
542
|
+
if (val.length === 0)
|
|
543
|
+
return "[]";
|
|
544
|
+
if (indent > 0) {
|
|
545
|
+
return JSON.stringify(val);
|
|
546
|
+
}
|
|
547
|
+
const lines = [""];
|
|
548
|
+
for (const item of val) {
|
|
549
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
550
|
+
const entries = Object.entries(item).filter(([, v]) => v !== undefined);
|
|
551
|
+
if (entries.length === 0) {
|
|
552
|
+
lines.push(`${pad} - {}`);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const [firstK, firstV] = entries[0];
|
|
556
|
+
lines.push(`${pad} - ${firstK}: ${serializeYamlValue(firstV, indent + 4)}`);
|
|
557
|
+
for (let i = 1;i < entries.length; i++) {
|
|
558
|
+
const [k, v] = entries[i];
|
|
559
|
+
lines.push(`${pad} ${k}: ${serializeYamlValue(v, indent + 4)}`);
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
lines.push(`${pad} - ${serializeYamlValue(item, indent + 2)}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return lines.join(`
|
|
566
|
+
`);
|
|
567
|
+
}
|
|
568
|
+
return JSON.stringify(val);
|
|
569
|
+
}
|
|
570
|
+
function stringifyDocument(data, body = "") {
|
|
571
|
+
const lines = ["---"];
|
|
572
|
+
for (const [key, value] of Object.entries(data)) {
|
|
573
|
+
if (value === undefined)
|
|
574
|
+
continue;
|
|
575
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
576
|
+
lines.push(`${key}:${serializeYamlValue(value, 0)}`);
|
|
577
|
+
} else {
|
|
578
|
+
lines.push(`${key}: ${serializeYamlValue(value, 0)}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
lines.push("---");
|
|
582
|
+
lines.push("");
|
|
583
|
+
const trimmedBody = body.replace(/\r\n/g, `
|
|
584
|
+
`).replace(/^\n+/, "").replace(/\n+$/, "");
|
|
585
|
+
if (trimmedBody.length > 0) {
|
|
586
|
+
lines.push(trimmedBody);
|
|
587
|
+
lines.push("");
|
|
588
|
+
}
|
|
589
|
+
return lines.join(`
|
|
590
|
+
`);
|
|
591
|
+
}
|
|
592
|
+
function atomicWriteFile(targetPath, content) {
|
|
593
|
+
const dir = dirname(targetPath);
|
|
594
|
+
if (!existsSync(dir)) {
|
|
595
|
+
mkdirSync(dir, { recursive: true });
|
|
596
|
+
}
|
|
597
|
+
const tmpPath = `${targetPath}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
598
|
+
try {
|
|
599
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
600
|
+
renameSync(tmpPath, targetPath);
|
|
601
|
+
} catch (err) {
|
|
602
|
+
try {
|
|
603
|
+
if (existsSync(tmpPath)) {
|
|
604
|
+
unlinkSync(tmpPath);
|
|
605
|
+
}
|
|
606
|
+
} catch {}
|
|
607
|
+
throw err;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// db/wal/index.ts
|
|
612
|
+
import { join } from "path";
|
|
613
|
+
|
|
614
|
+
// db/wal/config.ts
|
|
615
|
+
var WAL_CONFIG_DEFAULTS = {
|
|
616
|
+
RING_BUFFER_CAPACITY: Number(process.env.SYNAPSE_WAL_RING_CAPACITY) || 4096,
|
|
617
|
+
ARENA_INITIAL_CAPACITY: Number(process.env.SYNAPSE_WAL_ARENA_CAPACITY) || 256,
|
|
618
|
+
ARENA_MAX_CAPACITY: Number(process.env.SYNAPSE_WAL_ARENA_MAX_CAPACITY) || 65536,
|
|
619
|
+
SQLITE_WAL_FILENAME: process.env.SYNAPSE_WAL_SQLITE_FILENAME || "synapse_causal_wal.sqlite",
|
|
620
|
+
SQLITE_MMAP_SIZE_BYTES: Number(process.env.SYNAPSE_WAL_MMAP_BYTES) || 268435456,
|
|
621
|
+
SQLITE_BUSY_TIMEOUT_MS: Number(process.env.SYNAPSE_WAL_BUSY_TIMEOUT_MS) || 5000,
|
|
622
|
+
SQLITE_SYNC_MODE: process.env.SYNAPSE_WAL_SYNC_MODE || "NORMAL"
|
|
623
|
+
};
|
|
624
|
+
var CausalWalEventType;
|
|
625
|
+
((CausalWalEventType) => {
|
|
626
|
+
CausalWalEventType["DOC_UPSERT"] = "DOC_UPSERT";
|
|
627
|
+
CausalWalEventType["DOC_DELETE"] = "DOC_DELETE";
|
|
628
|
+
CausalWalEventType["EDGE_LINK"] = "EDGE_LINK";
|
|
629
|
+
CausalWalEventType["EDGE_UNLINK"] = "EDGE_UNLINK";
|
|
630
|
+
CausalWalEventType["COMPILER_METRIC"] = "COMPILER_METRIC";
|
|
631
|
+
CausalWalEventType["CHECKPOINT_MARK"] = "CHECKPOINT_MARK";
|
|
632
|
+
})(CausalWalEventType ||= {});
|
|
633
|
+
|
|
634
|
+
// db/wal/ringBuffer.ts
|
|
635
|
+
class TemporalRingBuffer {
|
|
636
|
+
capacity;
|
|
637
|
+
isPowerOfTwo;
|
|
638
|
+
bitMask;
|
|
639
|
+
slots;
|
|
640
|
+
lsnBuffer;
|
|
641
|
+
tsBuffer;
|
|
642
|
+
head = 0;
|
|
643
|
+
tail = 0;
|
|
644
|
+
count = 0;
|
|
645
|
+
nextLsn = 1;
|
|
646
|
+
overwrittenCount = 0;
|
|
647
|
+
constructor(capacity = WAL_CONFIG_DEFAULTS.RING_BUFFER_CAPACITY) {
|
|
648
|
+
if (!Number.isInteger(capacity) || capacity <= 0) {
|
|
649
|
+
throw new Error(`[TemporalRingBuffer] \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uB9C1 \uBC84\uD37C \uC6A9\uB7C9\uC785\uB2C8\uB2E4: ${capacity}`);
|
|
650
|
+
}
|
|
651
|
+
this.capacity = capacity;
|
|
652
|
+
this.isPowerOfTwo = (capacity & capacity - 1) === 0;
|
|
653
|
+
this.bitMask = capacity - 1;
|
|
654
|
+
this.slots = new Array(capacity);
|
|
655
|
+
this.lsnBuffer = new Float64Array(capacity);
|
|
656
|
+
this.tsBuffer = new Float64Array(capacity);
|
|
657
|
+
}
|
|
658
|
+
wrapIndex(idx) {
|
|
659
|
+
return this.isPowerOfTwo ? idx & this.bitMask : idx % this.capacity;
|
|
660
|
+
}
|
|
661
|
+
append(entryInput) {
|
|
662
|
+
const lsn = this.nextLsn++;
|
|
663
|
+
const timestampMs = entryInput.timestampMs ?? Date.now();
|
|
664
|
+
const entry = {
|
|
665
|
+
lsn,
|
|
666
|
+
timestampMs,
|
|
667
|
+
eventType: entryInput.eventType,
|
|
668
|
+
collection: entryInput.collection,
|
|
669
|
+
docId: entryInput.docId,
|
|
670
|
+
prevDoc: entryInput.prevDoc,
|
|
671
|
+
nextDoc: entryInput.nextDoc,
|
|
672
|
+
edgesAdded: entryInput.edgesAdded,
|
|
673
|
+
edgesRemoved: entryInput.edgesRemoved,
|
|
674
|
+
compilerMetric: entryInput.compilerMetric
|
|
675
|
+
};
|
|
676
|
+
this.writeRawEntry(entry);
|
|
677
|
+
return entry;
|
|
678
|
+
}
|
|
679
|
+
appendRecovered(entry) {
|
|
680
|
+
if (entry.lsn >= this.nextLsn) {
|
|
681
|
+
this.nextLsn = entry.lsn + 1;
|
|
682
|
+
}
|
|
683
|
+
this.writeRawEntry(entry);
|
|
684
|
+
}
|
|
685
|
+
writeRawEntry(entry) {
|
|
686
|
+
const t = this.tail;
|
|
687
|
+
this.slots[t] = entry;
|
|
688
|
+
this.lsnBuffer[t] = entry.lsn;
|
|
689
|
+
this.tsBuffer[t] = entry.timestampMs;
|
|
690
|
+
const nextTail = this.isPowerOfTwo ? t + 1 & this.bitMask : (t + 1) % this.capacity;
|
|
691
|
+
this.tail = nextTail;
|
|
692
|
+
if (this.count === this.capacity) {
|
|
693
|
+
this.head = nextTail;
|
|
694
|
+
this.overwrittenCount++;
|
|
695
|
+
} else {
|
|
696
|
+
this.count++;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
getByLsn(targetLsn) {
|
|
700
|
+
if (this.count === 0)
|
|
701
|
+
return null;
|
|
702
|
+
const oldestLsn = this.lsnBuffer[this.head];
|
|
703
|
+
const newestIdx = this.wrapIndex(this.tail - 1 + this.capacity);
|
|
704
|
+
const newestLsn = this.lsnBuffer[newestIdx];
|
|
705
|
+
if (targetLsn < oldestLsn || targetLsn > newestLsn) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
const offset = targetLsn - oldestLsn;
|
|
709
|
+
if (offset < this.count) {
|
|
710
|
+
const candidateIdx = this.wrapIndex(this.head + offset);
|
|
711
|
+
if (this.lsnBuffer[candidateIdx] === targetLsn) {
|
|
712
|
+
return this.slots[candidateIdx] || null;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
for (let i = 0;i < this.count; i++) {
|
|
716
|
+
const idx = this.wrapIndex(this.head + i);
|
|
717
|
+
if (this.lsnBuffer[idx] === targetLsn) {
|
|
718
|
+
return this.slots[idx] || null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
forEachReverse(visitor) {
|
|
724
|
+
for (let i = this.count - 1;i >= 0; i--) {
|
|
725
|
+
const idx = (this.head + i) % this.capacity;
|
|
726
|
+
const entry = this.slots[idx];
|
|
727
|
+
if (entry) {
|
|
728
|
+
const shouldContinue = visitor(entry);
|
|
729
|
+
if (shouldContinue === false)
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
toArray() {
|
|
735
|
+
const result = new Array(this.count);
|
|
736
|
+
for (let i = 0;i < this.count; i++) {
|
|
737
|
+
const idx = (this.head + i) % this.capacity;
|
|
738
|
+
result[i] = this.slots[idx];
|
|
739
|
+
}
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
742
|
+
getEntriesSinceLsn(afterLsn) {
|
|
743
|
+
const result = [];
|
|
744
|
+
for (let i = 0;i < this.count; i++) {
|
|
745
|
+
const idx = (this.head + i) % this.capacity;
|
|
746
|
+
const entry = this.slots[idx];
|
|
747
|
+
if (entry && entry.lsn > afterLsn) {
|
|
748
|
+
result.push(entry);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return result;
|
|
752
|
+
}
|
|
753
|
+
getCurrentLsn() {
|
|
754
|
+
return this.nextLsn - 1;
|
|
755
|
+
}
|
|
756
|
+
getOldestLsn() {
|
|
757
|
+
if (this.count === 0)
|
|
758
|
+
return 0;
|
|
759
|
+
return this.lsnBuffer[this.head];
|
|
760
|
+
}
|
|
761
|
+
getStats() {
|
|
762
|
+
return {
|
|
763
|
+
capacity: this.capacity,
|
|
764
|
+
size: this.count,
|
|
765
|
+
currentLsn: this.getCurrentLsn(),
|
|
766
|
+
oldestLsn: this.getOldestLsn(),
|
|
767
|
+
overwrittenCount: this.overwrittenCount,
|
|
768
|
+
utilizationRatio: Number((this.count / this.capacity).toFixed(4))
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
clear() {
|
|
772
|
+
this.slots.fill(undefined);
|
|
773
|
+
this.lsnBuffer.fill(0);
|
|
774
|
+
this.tsBuffer.fill(0);
|
|
775
|
+
this.head = 0;
|
|
776
|
+
this.tail = 0;
|
|
777
|
+
this.count = 0;
|
|
778
|
+
this.nextLsn = 1;
|
|
779
|
+
this.overwrittenCount = 0;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// db/wal/arena.ts
|
|
784
|
+
class CausalArenaAllocator {
|
|
785
|
+
pool;
|
|
786
|
+
cursor = 0;
|
|
787
|
+
inTransaction = false;
|
|
788
|
+
totalCommitted = 0;
|
|
789
|
+
totalRolledBack = 0;
|
|
790
|
+
totalSlotReuses = 0;
|
|
791
|
+
constructor(initialCapacity = WAL_CONFIG_DEFAULTS.ARENA_INITIAL_CAPACITY) {
|
|
792
|
+
this.pool = new Array(initialCapacity);
|
|
793
|
+
for (let i = 0;i < initialCapacity; i++) {
|
|
794
|
+
this.pool[i] = this.createEmptySlot();
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
createEmptySlot() {
|
|
798
|
+
return {
|
|
799
|
+
eventType: "DOC_UPSERT" /* DOC_UPSERT */,
|
|
800
|
+
collection: "",
|
|
801
|
+
docId: "",
|
|
802
|
+
prevDoc: null,
|
|
803
|
+
nextDoc: null,
|
|
804
|
+
edgesAdded: [],
|
|
805
|
+
edgesRemoved: [],
|
|
806
|
+
rawMarkdown: null,
|
|
807
|
+
targetDiskPath: null,
|
|
808
|
+
compilerMetric: undefined
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
beginScope() {
|
|
812
|
+
if (this.inTransaction) {
|
|
813
|
+
throw new Error("[CausalArenaAllocator] \uC911\uCCA9 \uD2B8\uB79C\uC7AD\uC158\uC740 \uC774\uBBF8 \uD65C\uC131\uD654\uB41C \uC2A4\uCF54\uD504\uAC00 \uC874\uC7AC\uD569\uB2C8\uB2E4.");
|
|
814
|
+
}
|
|
815
|
+
this.inTransaction = true;
|
|
816
|
+
this.cursor = 0;
|
|
817
|
+
}
|
|
818
|
+
isScopeActive() {
|
|
819
|
+
return this.inTransaction;
|
|
820
|
+
}
|
|
821
|
+
allocSlot(eventType, collection, docId, prevDoc, nextDoc, edgesAdded, edgesRemoved, rawMarkdown = null, targetDiskPath = null, compilerMetric) {
|
|
822
|
+
if (this.cursor >= this.pool.length) {
|
|
823
|
+
if (this.pool.length >= WAL_CONFIG_DEFAULTS.ARENA_MAX_CAPACITY) {
|
|
824
|
+
throw new Error(`[CausalArenaAllocator] \uC544\uB808\uB098 \uCD5C\uB300 \uC6A9\uB7C9 \uC0C1\uD55C\uC120(${WAL_CONFIG_DEFAULTS.ARENA_MAX_CAPACITY})\uC744 \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4.`);
|
|
825
|
+
}
|
|
826
|
+
const expandCount = Math.min(this.pool.length, WAL_CONFIG_DEFAULTS.ARENA_MAX_CAPACITY - this.pool.length);
|
|
827
|
+
for (let i = 0;i < expandCount; i++) {
|
|
828
|
+
this.pool.push(this.createEmptySlot());
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
this.totalSlotReuses++;
|
|
832
|
+
}
|
|
833
|
+
const slot = this.pool[this.cursor++];
|
|
834
|
+
slot.eventType = eventType;
|
|
835
|
+
slot.collection = collection;
|
|
836
|
+
slot.docId = docId;
|
|
837
|
+
slot.prevDoc = prevDoc;
|
|
838
|
+
slot.nextDoc = nextDoc;
|
|
839
|
+
slot.edgesAdded = edgesAdded;
|
|
840
|
+
slot.edgesRemoved = edgesRemoved;
|
|
841
|
+
slot.rawMarkdown = rawMarkdown;
|
|
842
|
+
slot.targetDiskPath = targetDiskPath;
|
|
843
|
+
slot.compilerMetric = compilerMetric;
|
|
844
|
+
return slot;
|
|
845
|
+
}
|
|
846
|
+
getActiveSlots() {
|
|
847
|
+
return this.pool.slice(0, this.cursor);
|
|
848
|
+
}
|
|
849
|
+
commitAndReset() {
|
|
850
|
+
this.clearReferencesUpToCursor();
|
|
851
|
+
this.cursor = 0;
|
|
852
|
+
this.inTransaction = false;
|
|
853
|
+
this.totalCommitted++;
|
|
854
|
+
}
|
|
855
|
+
rollbackAndReset() {
|
|
856
|
+
this.clearReferencesUpToCursor();
|
|
857
|
+
this.cursor = 0;
|
|
858
|
+
this.inTransaction = false;
|
|
859
|
+
this.totalRolledBack++;
|
|
860
|
+
}
|
|
861
|
+
clearReferencesUpToCursor() {
|
|
862
|
+
for (let i = 0;i < this.cursor; i++) {
|
|
863
|
+
const slot = this.pool[i];
|
|
864
|
+
slot.prevDoc = null;
|
|
865
|
+
slot.nextDoc = null;
|
|
866
|
+
slot.rawMarkdown = null;
|
|
867
|
+
slot.targetDiskPath = null;
|
|
868
|
+
slot.compilerMetric = undefined;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
getStats() {
|
|
872
|
+
return {
|
|
873
|
+
poolCapacity: this.pool.length,
|
|
874
|
+
activeCursor: this.cursor,
|
|
875
|
+
totalTransactionsCommitted: this.totalCommitted,
|
|
876
|
+
totalTransactionsRolledBack: this.totalRolledBack,
|
|
877
|
+
totalSlotReuses: this.totalSlotReuses
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// db/wal/sqliteWalStore.ts
|
|
883
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
884
|
+
import { dirname as dirname2, resolve } from "path";
|
|
885
|
+
import { Database } from "bun:sqlite";
|
|
886
|
+
class SqliteCausalWalStore {
|
|
887
|
+
syncMode;
|
|
888
|
+
dbPath;
|
|
889
|
+
sqlite = null;
|
|
890
|
+
insertStmt = null;
|
|
891
|
+
batchTx = null;
|
|
892
|
+
lastFlushedLsn = 0;
|
|
893
|
+
constructor(dbPath, syncMode = WAL_CONFIG_DEFAULTS.SQLITE_SYNC_MODE) {
|
|
894
|
+
this.syncMode = syncMode;
|
|
895
|
+
this.dbPath = resolve(dbPath);
|
|
896
|
+
}
|
|
897
|
+
init() {
|
|
898
|
+
if (this.sqlite)
|
|
899
|
+
return;
|
|
900
|
+
try {
|
|
901
|
+
const parentDir = dirname2(this.dbPath);
|
|
902
|
+
if (!existsSync2(parentDir)) {
|
|
903
|
+
mkdirSync2(parentDir, { recursive: true });
|
|
904
|
+
}
|
|
905
|
+
this.sqlite = new Database(this.dbPath, { create: true });
|
|
906
|
+
this.sqlite.exec(`PRAGMA journal_mode = WAL;`);
|
|
907
|
+
this.sqlite.exec(`PRAGMA synchronous = ${this.syncMode};`);
|
|
908
|
+
this.sqlite.exec(`PRAGMA temp_store = MEMORY;`);
|
|
909
|
+
this.sqlite.exec(`PRAGMA mmap_size = ${WAL_CONFIG_DEFAULTS.SQLITE_MMAP_SIZE_BYTES};`);
|
|
910
|
+
this.sqlite.exec(`PRAGMA busy_timeout = ${WAL_CONFIG_DEFAULTS.SQLITE_BUSY_TIMEOUT_MS};`);
|
|
911
|
+
this.sqlite.exec(`
|
|
912
|
+
CREATE TABLE IF NOT EXISTS causal_wal_log (
|
|
913
|
+
lsn INTEGER PRIMARY KEY,
|
|
914
|
+
timestamp_ms INTEGER NOT NULL,
|
|
915
|
+
event_type TEXT NOT NULL,
|
|
916
|
+
collection TEXT NOT NULL,
|
|
917
|
+
doc_id TEXT NOT NULL,
|
|
918
|
+
prev_doc_json TEXT,
|
|
919
|
+
next_doc_json TEXT,
|
|
920
|
+
edges_added_json TEXT NOT NULL,
|
|
921
|
+
edges_removed_json TEXT NOT NULL,
|
|
922
|
+
compiler_metric_json TEXT
|
|
923
|
+
);
|
|
924
|
+
CREATE INDEX IF NOT EXISTS idx_causal_wal_ts ON causal_wal_log(timestamp_ms);
|
|
925
|
+
CREATE INDEX IF NOT EXISTS idx_causal_wal_doc ON causal_wal_log(collection, doc_id);
|
|
926
|
+
`);
|
|
927
|
+
const maxRow = this.sqlite.query(`SELECT MAX(lsn) as max_lsn FROM causal_wal_log`).get();
|
|
928
|
+
this.lastFlushedLsn = maxRow?.max_lsn ?? 0;
|
|
929
|
+
const stmt = this.sqlite.prepare(`
|
|
930
|
+
INSERT OR REPLACE INTO causal_wal_log (
|
|
931
|
+
lsn, timestamp_ms, event_type, collection, doc_id,
|
|
932
|
+
prev_doc_json, next_doc_json, edges_added_json, edges_removed_json, compiler_metric_json
|
|
933
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
934
|
+
`);
|
|
935
|
+
this.insertStmt = stmt;
|
|
936
|
+
this.batchTx = this.sqlite.transaction((items) => {
|
|
937
|
+
let written = 0;
|
|
938
|
+
for (let i = 0;i < items.length; i++) {
|
|
939
|
+
const item = items[i];
|
|
940
|
+
if (item.lsn <= this.lastFlushedLsn)
|
|
941
|
+
continue;
|
|
942
|
+
stmt.run(item.lsn, item.timestampMs, item.eventType, item.collection, item.docId, item.prevDoc ? JSON.stringify(item.prevDoc) : null, item.nextDoc ? JSON.stringify(item.nextDoc) : null, item.edgesAdded.length === 0 ? "[]" : JSON.stringify(item.edgesAdded), item.edgesRemoved.length === 0 ? "[]" : JSON.stringify(item.edgesRemoved), item.compilerMetric ? JSON.stringify(item.compilerMetric) : null);
|
|
943
|
+
if (item.lsn > this.lastFlushedLsn) {
|
|
944
|
+
this.lastFlushedLsn = item.lsn;
|
|
945
|
+
}
|
|
946
|
+
written++;
|
|
947
|
+
}
|
|
948
|
+
return written;
|
|
949
|
+
});
|
|
950
|
+
} catch (err) {
|
|
951
|
+
throw new Error(`[SqliteCausalWalStore] SQLite WAL \uCD08\uAE30\uD654 \uC2E4\uD328 (${this.dbPath}): ${err?.message || err}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
flushEntries(entries) {
|
|
955
|
+
if (!this.sqlite || !this.batchTx || entries.length === 0)
|
|
956
|
+
return 0;
|
|
957
|
+
try {
|
|
958
|
+
return this.batchTx(entries);
|
|
959
|
+
} catch (err) {
|
|
960
|
+
throw new Error(`[SqliteCausalWalStore] WAL \uC5D4\uD2B8\uB9AC \uBC30\uCE58 \uD50C\uB7EC\uC2DC \uC2E4\uD328: ${err?.message || err}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
loadAllEntries() {
|
|
964
|
+
if (!this.sqlite)
|
|
965
|
+
return [];
|
|
966
|
+
try {
|
|
967
|
+
const rows = this.sqlite.query(`SELECT * FROM causal_wal_log ORDER BY lsn ASC`).all();
|
|
968
|
+
return rows.map((row) => ({
|
|
969
|
+
lsn: row.lsn,
|
|
970
|
+
timestampMs: row.timestamp_ms,
|
|
971
|
+
eventType: row.event_type,
|
|
972
|
+
collection: row.collection,
|
|
973
|
+
docId: row.doc_id,
|
|
974
|
+
prevDoc: row.prev_doc_json ? JSON.parse(row.prev_doc_json) : null,
|
|
975
|
+
nextDoc: row.next_doc_json ? JSON.parse(row.next_doc_json) : null,
|
|
976
|
+
edgesAdded: JSON.parse(row.edges_added_json),
|
|
977
|
+
edgesRemoved: JSON.parse(row.edges_removed_json),
|
|
978
|
+
compilerMetric: row.compiler_metric_json ? JSON.parse(row.compiler_metric_json) : undefined
|
|
979
|
+
}));
|
|
980
|
+
} catch (err) {
|
|
981
|
+
throw new Error(`[SqliteCausalWalStore] WAL \uBCF5\uAD6C \uB85C\uADF8 \uB85C\uB4DC \uC2E4\uD328: ${err?.message || err}`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
getJournalMode() {
|
|
985
|
+
if (!this.sqlite)
|
|
986
|
+
return "closed";
|
|
987
|
+
const row = this.sqlite.query(`PRAGMA journal_mode;`).get();
|
|
988
|
+
return row?.journal_mode ?? "unknown";
|
|
989
|
+
}
|
|
990
|
+
getLastFlushedLsn() {
|
|
991
|
+
return this.lastFlushedLsn;
|
|
992
|
+
}
|
|
993
|
+
getPersistedCount() {
|
|
994
|
+
if (!this.sqlite)
|
|
995
|
+
return 0;
|
|
996
|
+
const row = this.sqlite.query(`SELECT COUNT(*) as cnt FROM causal_wal_log`).get();
|
|
997
|
+
return row?.cnt ?? 0;
|
|
998
|
+
}
|
|
999
|
+
close() {
|
|
1000
|
+
if (!this.sqlite)
|
|
1001
|
+
return;
|
|
1002
|
+
try {
|
|
1003
|
+
this.insertStmt?.finalize();
|
|
1004
|
+
} catch {}
|
|
1005
|
+
this.insertStmt = null;
|
|
1006
|
+
this.batchTx = null;
|
|
1007
|
+
try {
|
|
1008
|
+
this.sqlite.exec(`PRAGMA wal_checkpoint(TRUNCATE);`);
|
|
1009
|
+
} catch {}
|
|
1010
|
+
try {
|
|
1011
|
+
this.sqlite.close();
|
|
1012
|
+
} catch {}
|
|
1013
|
+
this.sqlite = null;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// db/wal/timeTravel.ts
|
|
1018
|
+
class CausalTimeTravelEngine {
|
|
1019
|
+
static asOfLsn(currentConnectome, ringBuffer, targetLsn) {
|
|
1020
|
+
const currentLsn = ringBuffer.getCurrentLsn();
|
|
1021
|
+
if (targetLsn < 0) {
|
|
1022
|
+
throw new Error(`[CausalTimeTravelEngine] \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBAA9\uD45C LSN\uC785\uB2C8\uB2E4: ${targetLsn}`);
|
|
1023
|
+
}
|
|
1024
|
+
const docMap = new Map;
|
|
1025
|
+
for (const doc of currentConnectome.getAllDocuments()) {
|
|
1026
|
+
const nodeId = SynapseConnectome.makeNodeId(doc.collection, doc.id);
|
|
1027
|
+
docMap.set(nodeId, doc);
|
|
1028
|
+
}
|
|
1029
|
+
if (targetLsn < currentLsn) {
|
|
1030
|
+
ringBuffer.forEachReverse((entry) => {
|
|
1031
|
+
if (entry.lsn <= targetLsn) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
if (entry.eventType === "DOC_UPSERT" /* DOC_UPSERT */ || entry.eventType === "DOC_DELETE" /* DOC_DELETE */) {
|
|
1035
|
+
const nodeId = SynapseConnectome.makeNodeId(entry.collection, entry.docId);
|
|
1036
|
+
if (entry.prevDoc !== null) {
|
|
1037
|
+
docMap.set(nodeId, entry.prevDoc);
|
|
1038
|
+
} else {
|
|
1039
|
+
docMap.delete(nodeId);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return true;
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
const snapshotConnectome = new SynapseConnectome;
|
|
1046
|
+
for (const doc of docMap.values()) {
|
|
1047
|
+
snapshotConnectome.putDocument(doc);
|
|
1048
|
+
}
|
|
1049
|
+
return snapshotConnectome;
|
|
1050
|
+
}
|
|
1051
|
+
static asOf(currentConnectome, ringBuffer, timestampMsOrIso) {
|
|
1052
|
+
const targetMs = typeof timestampMsOrIso === "number" ? timestampMsOrIso : new Date(timestampMsOrIso).getTime();
|
|
1053
|
+
if (Number.isNaN(targetMs)) {
|
|
1054
|
+
throw new Error(`[CausalTimeTravelEngine] \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uD0C0\uC784\uC2A4\uD0EC\uD504 \uD615\uC2DD\uC785\uB2C8\uB2E4: ${timestampMsOrIso}`);
|
|
1055
|
+
}
|
|
1056
|
+
let resolvedLsn = 0;
|
|
1057
|
+
ringBuffer.forEachReverse((entry) => {
|
|
1058
|
+
if (entry.timestampMs <= targetMs) {
|
|
1059
|
+
resolvedLsn = entry.lsn;
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1062
|
+
return true;
|
|
1063
|
+
});
|
|
1064
|
+
return CausalTimeTravelEngine.asOfLsn(currentConnectome, ringBuffer, resolvedLsn);
|
|
1065
|
+
}
|
|
1066
|
+
static diffBetweenLsn(ringBuffer, fromLsn, toLsn) {
|
|
1067
|
+
const addedSet = new Set;
|
|
1068
|
+
const modifiedSet = new Set;
|
|
1069
|
+
const deletedSet = new Set;
|
|
1070
|
+
const linkedSet = new Set;
|
|
1071
|
+
const unlinkedSet = new Set;
|
|
1072
|
+
const compilerMetrics = [];
|
|
1073
|
+
const entries = ringBuffer.getEntriesSinceLsn(fromLsn);
|
|
1074
|
+
for (const entry of entries) {
|
|
1075
|
+
if (entry.lsn > toLsn)
|
|
1076
|
+
break;
|
|
1077
|
+
const nodeId = SynapseConnectome.makeNodeId(entry.collection, entry.docId);
|
|
1078
|
+
if (entry.eventType === "DOC_UPSERT" /* DOC_UPSERT */) {
|
|
1079
|
+
deletedSet.delete(nodeId);
|
|
1080
|
+
if (entry.prevDoc === null) {
|
|
1081
|
+
addedSet.add(nodeId);
|
|
1082
|
+
} else if (!addedSet.has(nodeId)) {
|
|
1083
|
+
modifiedSet.add(nodeId);
|
|
1084
|
+
}
|
|
1085
|
+
} else if (entry.eventType === "DOC_DELETE" /* DOC_DELETE */) {
|
|
1086
|
+
if (addedSet.has(nodeId)) {
|
|
1087
|
+
addedSet.delete(nodeId);
|
|
1088
|
+
} else {
|
|
1089
|
+
modifiedSet.delete(nodeId);
|
|
1090
|
+
deletedSet.add(nodeId);
|
|
1091
|
+
}
|
|
1092
|
+
} else if (entry.eventType === "COMPILER_METRIC" /* COMPILER_METRIC */ && entry.compilerMetric) {
|
|
1093
|
+
compilerMetrics.push(entry.compilerMetric);
|
|
1094
|
+
}
|
|
1095
|
+
for (const edge of entry.edgesAdded) {
|
|
1096
|
+
const edgeKey = `${nodeId}->${edge}`;
|
|
1097
|
+
unlinkedSet.delete(edgeKey);
|
|
1098
|
+
linkedSet.add(edgeKey);
|
|
1099
|
+
}
|
|
1100
|
+
for (const edge of entry.edgesRemoved) {
|
|
1101
|
+
const edgeKey = `${nodeId}->${edge}`;
|
|
1102
|
+
linkedSet.delete(edgeKey);
|
|
1103
|
+
unlinkedSet.add(edgeKey);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return {
|
|
1107
|
+
fromLsn,
|
|
1108
|
+
toLsn,
|
|
1109
|
+
addedDocuments: Array.from(addedSet),
|
|
1110
|
+
modifiedDocuments: Array.from(modifiedSet),
|
|
1111
|
+
deletedDocuments: Array.from(deletedSet),
|
|
1112
|
+
edgesLinked: Array.from(linkedSet),
|
|
1113
|
+
edgesUnlinked: Array.from(unlinkedSet),
|
|
1114
|
+
compilerMetrics
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// db/wal/index.ts
|
|
1120
|
+
var EMPTY_EDGES = [];
|
|
1121
|
+
var EMPTY_EDGE_DIFF = { edgesAdded: EMPTY_EDGES, edgesRemoved: EMPTY_EDGES };
|
|
1122
|
+
|
|
1123
|
+
class CausalWalEngine {
|
|
1124
|
+
ringBuffer;
|
|
1125
|
+
arena;
|
|
1126
|
+
sqliteStore;
|
|
1127
|
+
constructor(vaultDir, config2) {
|
|
1128
|
+
this.ringBuffer = new TemporalRingBuffer(config2?.ringCapacity ?? WAL_CONFIG_DEFAULTS.RING_BUFFER_CAPACITY);
|
|
1129
|
+
this.arena = new CausalArenaAllocator(config2?.arenaCapacity ?? WAL_CONFIG_DEFAULTS.ARENA_INITIAL_CAPACITY);
|
|
1130
|
+
if (config2?.persistSqlite || config2?.sqlitePath) {
|
|
1131
|
+
const targetSqlitePath = config2.sqlitePath || join(vaultDir, WAL_CONFIG_DEFAULTS.SQLITE_WAL_FILENAME);
|
|
1132
|
+
this.sqliteStore = new SqliteCausalWalStore(targetSqlitePath, config2.syncMode ?? WAL_CONFIG_DEFAULTS.SQLITE_SYNC_MODE);
|
|
1133
|
+
} else {
|
|
1134
|
+
this.sqliteStore = null;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
initAndRecover(connectome) {
|
|
1138
|
+
if (!this.sqliteStore)
|
|
1139
|
+
return 0;
|
|
1140
|
+
this.sqliteStore.init();
|
|
1141
|
+
const persistedEntries = this.sqliteStore.loadAllEntries();
|
|
1142
|
+
let recoveredCount = 0;
|
|
1143
|
+
for (const entry of persistedEntries) {
|
|
1144
|
+
this.ringBuffer.appendRecovered(entry);
|
|
1145
|
+
if (entry.eventType === "DOC_UPSERT" /* DOC_UPSERT */ && entry.nextDoc) {
|
|
1146
|
+
connectome.putDocument(entry.nextDoc);
|
|
1147
|
+
recoveredCount++;
|
|
1148
|
+
} else if (entry.eventType === "DOC_DELETE" /* DOC_DELETE */) {
|
|
1149
|
+
connectome.removeDocument(entry.collection, entry.docId);
|
|
1150
|
+
recoveredCount++;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return recoveredCount;
|
|
1154
|
+
}
|
|
1155
|
+
static computeEdgeDiff(prevDoc, nextDoc) {
|
|
1156
|
+
const prevLinks = prevDoc ? prevDoc.wikilinks : EMPTY_EDGES;
|
|
1157
|
+
const nextLinks = nextDoc ? nextDoc.wikilinks : EMPTY_EDGES;
|
|
1158
|
+
const prevLen = prevLinks.length;
|
|
1159
|
+
const nextLen = nextLinks.length;
|
|
1160
|
+
if (prevLen === 0 && nextLen === 0) {
|
|
1161
|
+
return EMPTY_EDGE_DIFF;
|
|
1162
|
+
}
|
|
1163
|
+
if (prevLen === 0 && nextLen === 1 && nextDoc) {
|
|
1164
|
+
const l = nextDoc.wikilinks[0];
|
|
1165
|
+
return {
|
|
1166
|
+
edgesAdded: [SynapseConnectome.makeNodeId(l.targetCollection || nextDoc.collection, l.targetId)],
|
|
1167
|
+
edgesRemoved: EMPTY_EDGES
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
if (prevLen === 1 && nextLen === 1 && prevDoc && nextDoc) {
|
|
1171
|
+
const pl = prevDoc.wikilinks[0];
|
|
1172
|
+
const nl = nextDoc.wikilinks[0];
|
|
1173
|
+
const pTarget = SynapseConnectome.makeNodeId(pl.targetCollection || prevDoc.collection, pl.targetId);
|
|
1174
|
+
const nTarget = SynapseConnectome.makeNodeId(nl.targetCollection || nextDoc.collection, nl.targetId);
|
|
1175
|
+
if (pTarget === nTarget) {
|
|
1176
|
+
return EMPTY_EDGE_DIFF;
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
edgesAdded: [nTarget],
|
|
1180
|
+
edgesRemoved: [pTarget]
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
const prevTargets = new Set;
|
|
1184
|
+
const nextTargets = new Set;
|
|
1185
|
+
if (prevDoc) {
|
|
1186
|
+
for (let i = 0;i < prevLen; i++) {
|
|
1187
|
+
const link = prevDoc.wikilinks[i];
|
|
1188
|
+
prevTargets.add(SynapseConnectome.makeNodeId(link.targetCollection || prevDoc.collection, link.targetId));
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (nextDoc) {
|
|
1192
|
+
for (let i = 0;i < nextLen; i++) {
|
|
1193
|
+
const link = nextDoc.wikilinks[i];
|
|
1194
|
+
nextTargets.add(SynapseConnectome.makeNodeId(link.targetCollection || nextDoc.collection, link.targetId));
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const edgesAdded = [];
|
|
1198
|
+
const edgesRemoved = [];
|
|
1199
|
+
for (const target of nextTargets) {
|
|
1200
|
+
if (!prevTargets.has(target)) {
|
|
1201
|
+
edgesAdded.push(target);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
for (const target of prevTargets) {
|
|
1205
|
+
if (!nextTargets.has(target)) {
|
|
1206
|
+
edgesRemoved.push(target);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return { edgesAdded, edgesRemoved };
|
|
1210
|
+
}
|
|
1211
|
+
recordDocumentUpsert(prevDoc, nextDoc, rawMarkdown = null, targetDiskPath = null, timestampMs) {
|
|
1212
|
+
const { edgesAdded, edgesRemoved } = CausalWalEngine.computeEdgeDiff(prevDoc, nextDoc);
|
|
1213
|
+
if (this.arena.isScopeActive()) {
|
|
1214
|
+
this.arena.allocSlot("DOC_UPSERT" /* DOC_UPSERT */, nextDoc.collection, nextDoc.id, prevDoc, nextDoc, edgesAdded, edgesRemoved, rawMarkdown, targetDiskPath);
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
const entry = this.ringBuffer.append({
|
|
1218
|
+
timestampMs,
|
|
1219
|
+
eventType: "DOC_UPSERT" /* DOC_UPSERT */,
|
|
1220
|
+
collection: nextDoc.collection,
|
|
1221
|
+
docId: nextDoc.id,
|
|
1222
|
+
prevDoc,
|
|
1223
|
+
nextDoc,
|
|
1224
|
+
edgesAdded,
|
|
1225
|
+
edgesRemoved
|
|
1226
|
+
});
|
|
1227
|
+
if (this.sqliteStore) {
|
|
1228
|
+
this.sqliteStore.flushEntries([entry]);
|
|
1229
|
+
}
|
|
1230
|
+
return entry;
|
|
1231
|
+
}
|
|
1232
|
+
recordDocumentDelete(prevDoc, targetDiskPath = null, timestampMs) {
|
|
1233
|
+
const { edgesAdded, edgesRemoved } = CausalWalEngine.computeEdgeDiff(prevDoc, null);
|
|
1234
|
+
if (this.arena.isScopeActive()) {
|
|
1235
|
+
this.arena.allocSlot("DOC_DELETE" /* DOC_DELETE */, prevDoc.collection, prevDoc.id, prevDoc, null, edgesAdded, edgesRemoved, null, targetDiskPath);
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
const entry = this.ringBuffer.append({
|
|
1239
|
+
timestampMs,
|
|
1240
|
+
eventType: "DOC_DELETE" /* DOC_DELETE */,
|
|
1241
|
+
collection: prevDoc.collection,
|
|
1242
|
+
docId: prevDoc.id,
|
|
1243
|
+
prevDoc,
|
|
1244
|
+
nextDoc: null,
|
|
1245
|
+
edgesAdded,
|
|
1246
|
+
edgesRemoved
|
|
1247
|
+
});
|
|
1248
|
+
if (this.sqliteStore) {
|
|
1249
|
+
this.sqliteStore.flushEntries([entry]);
|
|
1250
|
+
}
|
|
1251
|
+
return entry;
|
|
1252
|
+
}
|
|
1253
|
+
recordCompilerMetric(metric, timestampMs) {
|
|
1254
|
+
const entry = this.ringBuffer.append({
|
|
1255
|
+
timestampMs,
|
|
1256
|
+
eventType: "COMPILER_METRIC" /* COMPILER_METRIC */,
|
|
1257
|
+
collection: "_compiler_metrics",
|
|
1258
|
+
docId: metric.targetFile,
|
|
1259
|
+
prevDoc: null,
|
|
1260
|
+
nextDoc: null,
|
|
1261
|
+
edgesAdded: [],
|
|
1262
|
+
edgesRemoved: [],
|
|
1263
|
+
compilerMetric: metric
|
|
1264
|
+
});
|
|
1265
|
+
if (this.sqliteStore) {
|
|
1266
|
+
this.sqliteStore.flushEntries([entry]);
|
|
1267
|
+
}
|
|
1268
|
+
return entry;
|
|
1269
|
+
}
|
|
1270
|
+
executeArenaTransaction(fn, onCommitSlot) {
|
|
1271
|
+
this.arena.beginScope();
|
|
1272
|
+
try {
|
|
1273
|
+
const result = fn();
|
|
1274
|
+
const activeSlots = this.arena.getActiveSlots();
|
|
1275
|
+
const committedEntries = [];
|
|
1276
|
+
for (const slot of activeSlots) {
|
|
1277
|
+
onCommitSlot(slot);
|
|
1278
|
+
const entry = this.ringBuffer.append({
|
|
1279
|
+
eventType: slot.eventType,
|
|
1280
|
+
collection: slot.collection,
|
|
1281
|
+
docId: slot.docId,
|
|
1282
|
+
prevDoc: slot.prevDoc,
|
|
1283
|
+
nextDoc: slot.nextDoc,
|
|
1284
|
+
edgesAdded: [...slot.edgesAdded],
|
|
1285
|
+
edgesRemoved: [...slot.edgesRemoved],
|
|
1286
|
+
compilerMetric: slot.compilerMetric
|
|
1287
|
+
});
|
|
1288
|
+
committedEntries.push(entry);
|
|
1289
|
+
}
|
|
1290
|
+
this.arena.commitAndReset();
|
|
1291
|
+
if (this.sqliteStore && committedEntries.length > 0) {
|
|
1292
|
+
this.sqliteStore.flushEntries(committedEntries);
|
|
1293
|
+
}
|
|
1294
|
+
return result;
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
this.arena.rollbackAndReset();
|
|
1297
|
+
throw err;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
async executeArenaTransactionAsync(fn, onCommitSlot) {
|
|
1301
|
+
this.arena.beginScope();
|
|
1302
|
+
try {
|
|
1303
|
+
const result = await fn();
|
|
1304
|
+
const activeSlots = this.arena.getActiveSlots();
|
|
1305
|
+
const committedEntries = [];
|
|
1306
|
+
for (const slot of activeSlots) {
|
|
1307
|
+
onCommitSlot(slot);
|
|
1308
|
+
const entry = this.ringBuffer.append({
|
|
1309
|
+
eventType: slot.eventType,
|
|
1310
|
+
collection: slot.collection,
|
|
1311
|
+
docId: slot.docId,
|
|
1312
|
+
prevDoc: slot.prevDoc,
|
|
1313
|
+
nextDoc: slot.nextDoc,
|
|
1314
|
+
edgesAdded: [...slot.edgesAdded],
|
|
1315
|
+
edgesRemoved: [...slot.edgesRemoved],
|
|
1316
|
+
compilerMetric: slot.compilerMetric
|
|
1317
|
+
});
|
|
1318
|
+
committedEntries.push(entry);
|
|
1319
|
+
}
|
|
1320
|
+
this.arena.commitAndReset();
|
|
1321
|
+
if (this.sqliteStore && committedEntries.length > 0) {
|
|
1322
|
+
this.sqliteStore.flushEntries(committedEntries);
|
|
1323
|
+
}
|
|
1324
|
+
return result;
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
this.arena.rollbackAndReset();
|
|
1327
|
+
throw err;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
asOfLsn(connectome, targetLsn) {
|
|
1331
|
+
return CausalTimeTravelEngine.asOfLsn(connectome, this.ringBuffer, targetLsn);
|
|
1332
|
+
}
|
|
1333
|
+
asOf(connectome, timestampMsOrIso) {
|
|
1334
|
+
return CausalTimeTravelEngine.asOf(connectome, this.ringBuffer, timestampMsOrIso);
|
|
1335
|
+
}
|
|
1336
|
+
diffBetweenLsn(fromLsn, toLsn) {
|
|
1337
|
+
return CausalTimeTravelEngine.diffBetweenLsn(this.ringBuffer, fromLsn, toLsn);
|
|
1338
|
+
}
|
|
1339
|
+
getStats() {
|
|
1340
|
+
return {
|
|
1341
|
+
ringBuffer: this.ringBuffer.getStats(),
|
|
1342
|
+
arena: this.arena.getStats(),
|
|
1343
|
+
sqlite: {
|
|
1344
|
+
enabled: this.sqliteStore !== null,
|
|
1345
|
+
dbPath: this.sqliteStore?.dbPath ?? null,
|
|
1346
|
+
journalMode: this.sqliteStore ? this.sqliteStore.getJournalMode() : "disabled",
|
|
1347
|
+
lastFlushedLsn: this.sqliteStore ? this.sqliteStore.getLastFlushedLsn() : 0,
|
|
1348
|
+
persistedCount: this.sqliteStore ? this.sqliteStore.getPersistedCount() : 0
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
close() {
|
|
1353
|
+
if (this.sqliteStore) {
|
|
1354
|
+
this.sqliteStore.close();
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// db/engine.ts
|
|
1360
|
+
class SynapseEngine {
|
|
1361
|
+
vaultDir;
|
|
1362
|
+
connectome;
|
|
1363
|
+
wal;
|
|
1364
|
+
config;
|
|
1365
|
+
isInitialized = false;
|
|
1366
|
+
pendingWriteQueue = new Map;
|
|
1367
|
+
writeTimer = null;
|
|
1368
|
+
nextTimerFlushAtMs = 0;
|
|
1369
|
+
cachedIsoMs = -1;
|
|
1370
|
+
cachedIsoStr = "";
|
|
1371
|
+
watcherDebounceTimer = null;
|
|
1372
|
+
internalWriteResetTimer = null;
|
|
1373
|
+
fileWatcher = null;
|
|
1374
|
+
isWritingInternally = false;
|
|
1375
|
+
constructor(config) {
|
|
1376
|
+
this.config = {
|
|
1377
|
+
autoWatch: true,
|
|
1378
|
+
persistDebounceMs: 100,
|
|
1379
|
+
...config,
|
|
1380
|
+
vaultDir: resolve2(config.vaultDir)
|
|
1381
|
+
};
|
|
1382
|
+
this.vaultDir = this.config.vaultDir;
|
|
1383
|
+
this.connectome = new SynapseConnectome;
|
|
1384
|
+
this.wal = new CausalWalEngine(this.vaultDir, this.config.wal);
|
|
1385
|
+
}
|
|
1386
|
+
async init() {
|
|
1387
|
+
if (this.isInitialized)
|
|
1388
|
+
return;
|
|
1389
|
+
if (!existsSync3(this.vaultDir)) {
|
|
1390
|
+
mkdirSync3(this.vaultDir, { recursive: true });
|
|
1391
|
+
}
|
|
1392
|
+
this.reloadVault();
|
|
1393
|
+
this.wal.initAndRecover(this.connectome);
|
|
1394
|
+
if (this.config.autoWatch) {
|
|
1395
|
+
this.startWatcher();
|
|
1396
|
+
}
|
|
1397
|
+
this.isInitialized = true;
|
|
1398
|
+
}
|
|
1399
|
+
reloadVault() {
|
|
1400
|
+
this.connectome.clear();
|
|
1401
|
+
if (!existsSync3(this.vaultDir))
|
|
1402
|
+
return;
|
|
1403
|
+
const entries = readdirSync(this.vaultDir, { withFileTypes: true });
|
|
1404
|
+
for (const entry of entries) {
|
|
1405
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
1406
|
+
const collectionName = entry.name;
|
|
1407
|
+
const colPath = join2(this.vaultDir, collectionName);
|
|
1408
|
+
this.loadCollectionDir(collectionName, colPath);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
loadCollectionDir(collectionName, dirPath) {
|
|
1413
|
+
try {
|
|
1414
|
+
const files = readdirSync(dirPath);
|
|
1415
|
+
for (const file of files) {
|
|
1416
|
+
if (!file.endsWith(".md") || file.includes(".tmp"))
|
|
1417
|
+
continue;
|
|
1418
|
+
const id = basename(file, ".md");
|
|
1419
|
+
const fullPath = join2(dirPath, file);
|
|
1420
|
+
try {
|
|
1421
|
+
const raw = readFileSync2(fullPath, "utf-8");
|
|
1422
|
+
const parsed = parseMarkdownFile(raw, collectionName);
|
|
1423
|
+
const rawFm = { ...parsed.frontmatter };
|
|
1424
|
+
const createdAt = typeof rawFm._createdAt === "string" ? rawFm._createdAt : typeof rawFm.createdAt === "string" ? rawFm.createdAt : new Date().toISOString();
|
|
1425
|
+
const updatedAt = typeof rawFm._updatedAt === "string" ? rawFm._updatedAt : typeof rawFm.updatedAt === "string" ? rawFm.updatedAt : new Date().toISOString();
|
|
1426
|
+
delete rawFm._createdAt;
|
|
1427
|
+
delete rawFm._updatedAt;
|
|
1428
|
+
const doc = {
|
|
1429
|
+
id,
|
|
1430
|
+
collection: collectionName,
|
|
1431
|
+
data: rawFm,
|
|
1432
|
+
body: parsed.body,
|
|
1433
|
+
createdAt,
|
|
1434
|
+
updatedAt,
|
|
1435
|
+
wikilinks: parsed.wikilinks
|
|
1436
|
+
};
|
|
1437
|
+
this.connectome.putDocument(doc);
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
process.stderr.write(`[SynapseEngine] \uD30C\uC77C \uB85C\uB4DC \uC2E4\uD328 (${file}): ${err?.message || err}
|
|
1440
|
+
`);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
} catch {}
|
|
1444
|
+
}
|
|
1445
|
+
getEffectiveDocument(collection, id) {
|
|
1446
|
+
if (this.wal.arena.isScopeActive()) {
|
|
1447
|
+
const slots = this.wal.arena.getActiveSlots();
|
|
1448
|
+
for (let i = slots.length - 1;i >= 0; i--) {
|
|
1449
|
+
const slot = slots[i];
|
|
1450
|
+
if (slot.collection === collection && slot.docId === id) {
|
|
1451
|
+
if (slot.eventType === "DOC_DELETE" /* DOC_DELETE */)
|
|
1452
|
+
return null;
|
|
1453
|
+
if (slot.eventType === "DOC_UPSERT" /* DOC_UPSERT */) {
|
|
1454
|
+
return slot.nextDoc || null;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
return this.connectome.getDocument(collection, id);
|
|
1460
|
+
}
|
|
1461
|
+
isValidSafeIdentifier(str) {
|
|
1462
|
+
for (let i = 0;i < str.length; i++) {
|
|
1463
|
+
const code = str.charCodeAt(i);
|
|
1464
|
+
if (code === 47 || code === 92 || code === 58 || code === 42 || code === 63 || code === 34 || code === 60 || code === 62 || code === 124) {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
getNowIso(nowMs) {
|
|
1471
|
+
if (nowMs === this.cachedIsoMs) {
|
|
1472
|
+
return this.cachedIsoStr;
|
|
1473
|
+
}
|
|
1474
|
+
this.cachedIsoMs = nowMs;
|
|
1475
|
+
this.cachedIsoStr = new Date(nowMs).toISOString();
|
|
1476
|
+
return this.cachedIsoStr;
|
|
1477
|
+
}
|
|
1478
|
+
async saveDocument(collection, input) {
|
|
1479
|
+
const nowMs = Date.now();
|
|
1480
|
+
const now = this.getNowIso(nowMs);
|
|
1481
|
+
const existing = this.getEffectiveDocument(collection, input.id);
|
|
1482
|
+
const createdAt = input.createdAt || existing?.createdAt || now;
|
|
1483
|
+
const updatedAt = input.updatedAt || now;
|
|
1484
|
+
const rawBody = input.body || "";
|
|
1485
|
+
const cleanBody = rawBody.length > 0 && (rawBody.includes("\r") || rawBody.charCodeAt(0) === 10 || rawBody.charCodeAt(rawBody.length - 1) === 10) ? rawBody.replace(/\r\n/g, `
|
|
1486
|
+
`).replace(/^\n+/, "").replace(/\n+$/, "") : rawBody;
|
|
1487
|
+
const wikilinks = extractWikilinksFromDataAndBody(input.data, cleanBody, collection);
|
|
1488
|
+
const finalDoc = {
|
|
1489
|
+
id: input.id,
|
|
1490
|
+
collection,
|
|
1491
|
+
data: input.data,
|
|
1492
|
+
body: cleanBody,
|
|
1493
|
+
createdAt,
|
|
1494
|
+
updatedAt,
|
|
1495
|
+
wikilinks
|
|
1496
|
+
};
|
|
1497
|
+
const safeCol = this.isValidSafeIdentifier(collection) ? collection : collection.replace(/[\/\\:*?"<>|]/g, "_");
|
|
1498
|
+
const safeId = this.isValidSafeIdentifier(input.id) ? input.id : input.id.replace(/[\/\\:*?"<>|]/g, "_");
|
|
1499
|
+
const targetPath = `${this.vaultDir}/${safeCol}/${safeId}.md`;
|
|
1500
|
+
this.wal.recordDocumentUpsert(existing, finalDoc, null, targetPath, nowMs);
|
|
1501
|
+
if (this.wal.arena.isScopeActive()) {
|
|
1502
|
+
return finalDoc;
|
|
1503
|
+
}
|
|
1504
|
+
this.connectome.putDocument(finalDoc);
|
|
1505
|
+
const writeKey = `${collection}/${input.id}`;
|
|
1506
|
+
this.scheduleWrite(writeKey, nowMs, () => {
|
|
1507
|
+
this.markInternalWrite();
|
|
1508
|
+
const rawMarkdown = stringifyDocument({
|
|
1509
|
+
...finalDoc.data,
|
|
1510
|
+
_createdAt: finalDoc.createdAt,
|
|
1511
|
+
_updatedAt: finalDoc.updatedAt
|
|
1512
|
+
}, finalDoc.body);
|
|
1513
|
+
atomicWriteFile(targetPath, rawMarkdown);
|
|
1514
|
+
});
|
|
1515
|
+
return finalDoc;
|
|
1516
|
+
}
|
|
1517
|
+
markInternalWrite() {
|
|
1518
|
+
this.isWritingInternally = true;
|
|
1519
|
+
if (this.internalWriteResetTimer) {
|
|
1520
|
+
clearTimeout(this.internalWriteResetTimer);
|
|
1521
|
+
}
|
|
1522
|
+
this.internalWriteResetTimer = setTimeout(() => {
|
|
1523
|
+
this.isWritingInternally = false;
|
|
1524
|
+
this.internalWriteResetTimer = null;
|
|
1525
|
+
}, 120);
|
|
1526
|
+
}
|
|
1527
|
+
async deleteDocument(collection, id) {
|
|
1528
|
+
const existing = this.getEffectiveDocument(collection, id);
|
|
1529
|
+
if (!existing)
|
|
1530
|
+
return false;
|
|
1531
|
+
const safeCol = collection.replace(/[\/\\:*?"<>|]/g, "_");
|
|
1532
|
+
const safeId = id.replace(/[\/\\:*?"<>|]/g, "_");
|
|
1533
|
+
const targetPath = join2(this.vaultDir, safeCol, `${safeId}.md`);
|
|
1534
|
+
this.wal.recordDocumentDelete(existing, targetPath);
|
|
1535
|
+
if (this.wal.arena.isScopeActive()) {
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1538
|
+
const deleted = this.connectome.removeDocument(collection, id);
|
|
1539
|
+
if (!deleted)
|
|
1540
|
+
return false;
|
|
1541
|
+
const writeKey = `${collection}/${id}`;
|
|
1542
|
+
this.pendingWriteQueue.delete(writeKey);
|
|
1543
|
+
try {
|
|
1544
|
+
if (existsSync3(targetPath)) {
|
|
1545
|
+
this.markInternalWrite();
|
|
1546
|
+
unlinkSync2(targetPath);
|
|
1547
|
+
}
|
|
1548
|
+
} catch {}
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1551
|
+
async executeTransaction(fn) {
|
|
1552
|
+
return this.wal.executeArenaTransactionAsync(fn, (slot) => {
|
|
1553
|
+
if (slot.eventType === "DOC_UPSERT" /* DOC_UPSERT */ && slot.nextDoc) {
|
|
1554
|
+
const committedDoc = slot.nextDoc;
|
|
1555
|
+
this.connectome.putDocument(committedDoc);
|
|
1556
|
+
if (slot.targetDiskPath) {
|
|
1557
|
+
const targetPath = slot.targetDiskPath;
|
|
1558
|
+
const writeKey = `${slot.collection}/${slot.docId}`;
|
|
1559
|
+
this.scheduleWrite(writeKey, Date.now(), () => {
|
|
1560
|
+
this.markInternalWrite();
|
|
1561
|
+
const rawMarkdown = slot.rawMarkdown ?? stringifyDocument({
|
|
1562
|
+
...committedDoc.data,
|
|
1563
|
+
_createdAt: committedDoc.createdAt,
|
|
1564
|
+
_updatedAt: committedDoc.updatedAt
|
|
1565
|
+
}, committedDoc.body);
|
|
1566
|
+
atomicWriteFile(targetPath, rawMarkdown);
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
} else if (slot.eventType === "DOC_DELETE" /* DOC_DELETE */) {
|
|
1570
|
+
this.connectome.removeDocument(slot.collection, slot.docId);
|
|
1571
|
+
const writeKey = `${slot.collection}/${slot.docId}`;
|
|
1572
|
+
this.pendingWriteQueue.delete(writeKey);
|
|
1573
|
+
if (slot.targetDiskPath) {
|
|
1574
|
+
try {
|
|
1575
|
+
if (existsSync3(slot.targetDiskPath)) {
|
|
1576
|
+
this.markInternalWrite();
|
|
1577
|
+
unlinkSync2(slot.targetDiskPath);
|
|
1578
|
+
}
|
|
1579
|
+
} catch {}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
scheduleWrite(key, nowMs, fn) {
|
|
1585
|
+
this.pendingWriteQueue.set(key, fn);
|
|
1586
|
+
const debounceMs = this.config.persistDebounceMs || 100;
|
|
1587
|
+
this.nextTimerFlushAtMs = nowMs + debounceMs;
|
|
1588
|
+
if (!this.writeTimer) {
|
|
1589
|
+
this.armWriteTimer(debounceMs);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
armWriteTimer(delayMs) {
|
|
1593
|
+
this.writeTimer = setTimeout(() => {
|
|
1594
|
+
const remaining = this.nextTimerFlushAtMs - Date.now();
|
|
1595
|
+
if (remaining > 5 && this.pendingWriteQueue.size > 0) {
|
|
1596
|
+
this.armWriteTimer(remaining);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
this.flushPendingWrites();
|
|
1600
|
+
}, delayMs);
|
|
1601
|
+
}
|
|
1602
|
+
flushPendingWrites() {
|
|
1603
|
+
if (this.writeTimer) {
|
|
1604
|
+
clearTimeout(this.writeTimer);
|
|
1605
|
+
this.writeTimer = null;
|
|
1606
|
+
}
|
|
1607
|
+
if (this.pendingWriteQueue.size === 0)
|
|
1608
|
+
return;
|
|
1609
|
+
for (const [key, fn] of this.pendingWriteQueue) {
|
|
1610
|
+
try {
|
|
1611
|
+
fn();
|
|
1612
|
+
} catch (err) {
|
|
1613
|
+
process.stderr.write(`[SynapseEngine] \uB514\uC2A4\uD06C \uC4F0\uAE30 \uC2E4\uD328 (${key}): ${err?.message || err}
|
|
1614
|
+
`);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
this.pendingWriteQueue.clear();
|
|
1618
|
+
}
|
|
1619
|
+
startWatcher() {
|
|
1620
|
+
try {
|
|
1621
|
+
this.fileWatcher = watch(this.vaultDir, { recursive: true }, (_eventType, filename) => {
|
|
1622
|
+
if (!filename || !filename.endsWith(".md") || filename.includes(".tmp"))
|
|
1623
|
+
return;
|
|
1624
|
+
if (this.isWritingInternally)
|
|
1625
|
+
return;
|
|
1626
|
+
if (this.watcherDebounceTimer) {
|
|
1627
|
+
clearTimeout(this.watcherDebounceTimer);
|
|
1628
|
+
}
|
|
1629
|
+
this.watcherDebounceTimer = setTimeout(() => {
|
|
1630
|
+
this.watcherDebounceTimer = null;
|
|
1631
|
+
if (!this.isWritingInternally) {
|
|
1632
|
+
this.reloadVault();
|
|
1633
|
+
}
|
|
1634
|
+
}, 80);
|
|
1635
|
+
});
|
|
1636
|
+
} catch {}
|
|
1637
|
+
}
|
|
1638
|
+
getStats() {
|
|
1639
|
+
return this.connectome.getStats();
|
|
1640
|
+
}
|
|
1641
|
+
close() {
|
|
1642
|
+
if (this.writeTimer) {
|
|
1643
|
+
clearTimeout(this.writeTimer);
|
|
1644
|
+
this.writeTimer = null;
|
|
1645
|
+
}
|
|
1646
|
+
if (this.watcherDebounceTimer) {
|
|
1647
|
+
clearTimeout(this.watcherDebounceTimer);
|
|
1648
|
+
this.watcherDebounceTimer = null;
|
|
1649
|
+
}
|
|
1650
|
+
if (this.internalWriteResetTimer) {
|
|
1651
|
+
clearTimeout(this.internalWriteResetTimer);
|
|
1652
|
+
this.internalWriteResetTimer = null;
|
|
1653
|
+
}
|
|
1654
|
+
this.flushPendingWrites();
|
|
1655
|
+
if (this.fileWatcher) {
|
|
1656
|
+
this.fileWatcher.close();
|
|
1657
|
+
this.fileWatcher = null;
|
|
1658
|
+
}
|
|
1659
|
+
this.wal.close();
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// db/collection.ts
|
|
1664
|
+
class SynapseCollection {
|
|
1665
|
+
name;
|
|
1666
|
+
engine;
|
|
1667
|
+
constructor(name, engine) {
|
|
1668
|
+
this.name = name;
|
|
1669
|
+
this.engine = engine;
|
|
1670
|
+
}
|
|
1671
|
+
async save(input) {
|
|
1672
|
+
return this.engine.saveDocument(this.name, input);
|
|
1673
|
+
}
|
|
1674
|
+
get(id) {
|
|
1675
|
+
return this.engine.connectome.getDocument(this.name, id);
|
|
1676
|
+
}
|
|
1677
|
+
has(id) {
|
|
1678
|
+
return this.engine.connectome.getDocument(this.name, id) !== null;
|
|
1679
|
+
}
|
|
1680
|
+
async delete(id) {
|
|
1681
|
+
return this.engine.deleteDocument(this.name, id);
|
|
1682
|
+
}
|
|
1683
|
+
query(options) {
|
|
1684
|
+
return this.engine.connectome.queryCollection(this.name, options);
|
|
1685
|
+
}
|
|
1686
|
+
all() {
|
|
1687
|
+
return this.engine.connectome.queryCollection(this.name);
|
|
1688
|
+
}
|
|
1689
|
+
count() {
|
|
1690
|
+
return this.engine.connectome.queryCollection(this.name).length;
|
|
1691
|
+
}
|
|
1692
|
+
search(queryText) {
|
|
1693
|
+
return this.engine.connectome.search(queryText, this.name);
|
|
1694
|
+
}
|
|
1695
|
+
synapses(id) {
|
|
1696
|
+
const engine = this.engine;
|
|
1697
|
+
const colName = this.name;
|
|
1698
|
+
return {
|
|
1699
|
+
outgoing: (targetCollectionFilter) => {
|
|
1700
|
+
return engine.connectome.getOutgoingDocuments(colName, id, targetCollectionFilter);
|
|
1701
|
+
},
|
|
1702
|
+
incoming: (sourceCollectionFilter) => {
|
|
1703
|
+
return engine.connectome.getIncomingDocuments(colName, id, sourceCollectionFilter);
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// db/migrator.ts
|
|
1710
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1711
|
+
|
|
1712
|
+
class SynapseMigrator {
|
|
1713
|
+
engine;
|
|
1714
|
+
constructor(engine) {
|
|
1715
|
+
this.engine = engine;
|
|
1716
|
+
}
|
|
1717
|
+
async migrateFromSqlite(sqliteDbPath, mappings) {
|
|
1718
|
+
if (!existsSync4(sqliteDbPath)) {
|
|
1719
|
+
throw new Error(`[SynapseMigrator] SQLite DB \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${sqliteDbPath}`);
|
|
1720
|
+
}
|
|
1721
|
+
for (const m of mappings) {
|
|
1722
|
+
if (!/^[a-zA-Z0-9_]+$/.test(m.tableName)) {
|
|
1723
|
+
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}`);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
const { Database } = await import("bun:sqlite");
|
|
1727
|
+
const sqlite = new Database(sqliteDbPath, { readonly: true });
|
|
1728
|
+
const stats = {};
|
|
1729
|
+
try {
|
|
1730
|
+
for (const m of mappings) {
|
|
1731
|
+
let count = 0;
|
|
1732
|
+
try {
|
|
1733
|
+
const rows = sqlite.query(`SELECT * FROM "${m.tableName}"`).all();
|
|
1734
|
+
for (const row of rows) {
|
|
1735
|
+
const mapped = m.mapRow(row);
|
|
1736
|
+
await this.engine.saveDocument(m.targetCollection, {
|
|
1737
|
+
id: mapped.id,
|
|
1738
|
+
data: mapped.data,
|
|
1739
|
+
body: mapped.body
|
|
1740
|
+
});
|
|
1741
|
+
count++;
|
|
1742
|
+
}
|
|
1743
|
+
} catch (err) {
|
|
1744
|
+
process.stderr.write(`[SynapseMigrator] \uD14C\uC774\uBE14(${m.tableName}) \uBCC0\uD658 \uC911 \uC624\uB958: ${err?.message || err}
|
|
1745
|
+
`);
|
|
1746
|
+
}
|
|
1747
|
+
stats[m.targetCollection] = count;
|
|
1748
|
+
}
|
|
1749
|
+
this.engine.flushPendingWrites();
|
|
1750
|
+
sqlite.close();
|
|
1751
|
+
return stats;
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
sqlite.close();
|
|
1754
|
+
throw err;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
async migrateFromJsonArray(targetCollection, items, mapItem) {
|
|
1758
|
+
let count = 0;
|
|
1759
|
+
for (const item of items) {
|
|
1760
|
+
const input = mapItem(item);
|
|
1761
|
+
await this.engine.saveDocument(targetCollection, input);
|
|
1762
|
+
count++;
|
|
1763
|
+
}
|
|
1764
|
+
this.engine.flushPendingWrites();
|
|
1765
|
+
return count;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// db/canvasExporter.ts
|
|
1770
|
+
var COLLECTION_COLOR_PALETTE = {
|
|
1771
|
+
guilds: "5",
|
|
1772
|
+
streamers: "4",
|
|
1773
|
+
clips: "1",
|
|
1774
|
+
categories: "2",
|
|
1775
|
+
settings: "6",
|
|
1776
|
+
users: "3"
|
|
1777
|
+
};
|
|
1778
|
+
function exportToObsidianCanvas(connectome, targetFilePath, options) {
|
|
1779
|
+
const nodeWidth = options?.nodeWidth || 280;
|
|
1780
|
+
const nodeHeight = options?.nodeHeight || 160;
|
|
1781
|
+
const hGap = options?.horizontalGap || 80;
|
|
1782
|
+
const vGap = options?.verticalGap || 60;
|
|
1783
|
+
const allDocs = connectome.getAllDocuments();
|
|
1784
|
+
const allEdges = connectome.getAllEdges();
|
|
1785
|
+
const groupedByCollection = new Map;
|
|
1786
|
+
for (const doc of allDocs) {
|
|
1787
|
+
let list = groupedByCollection.get(doc.collection);
|
|
1788
|
+
if (!list) {
|
|
1789
|
+
list = [];
|
|
1790
|
+
groupedByCollection.set(doc.collection, list);
|
|
1791
|
+
}
|
|
1792
|
+
list.push(doc);
|
|
1793
|
+
}
|
|
1794
|
+
const nodes = [];
|
|
1795
|
+
const nodeIdToCanvasId = new Map;
|
|
1796
|
+
let colIndex = 0;
|
|
1797
|
+
for (const [colName, docs] of groupedByCollection) {
|
|
1798
|
+
const color = COLLECTION_COLOR_PALETTE[colName] || "5";
|
|
1799
|
+
const x = colIndex * (nodeWidth + hGap);
|
|
1800
|
+
docs.forEach((doc, rowIndex) => {
|
|
1801
|
+
const internalId = `${doc.collection}/${doc.id}`;
|
|
1802
|
+
const canvasNodeId = `node_${doc.collection}_${doc.id.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|
1803
|
+
nodeIdToCanvasId.set(internalId, canvasNodeId);
|
|
1804
|
+
const y = rowIndex * (nodeHeight + vGap);
|
|
1805
|
+
nodes.push({
|
|
1806
|
+
id: canvasNodeId,
|
|
1807
|
+
type: "file",
|
|
1808
|
+
file: `${doc.collection}/${doc.id}.md`,
|
|
1809
|
+
x,
|
|
1810
|
+
y,
|
|
1811
|
+
width: nodeWidth,
|
|
1812
|
+
height: nodeHeight,
|
|
1813
|
+
color
|
|
1814
|
+
});
|
|
1815
|
+
});
|
|
1816
|
+
colIndex++;
|
|
1817
|
+
}
|
|
1818
|
+
const edges = [];
|
|
1819
|
+
let edgeCounter = 0;
|
|
1820
|
+
for (const edge of allEdges) {
|
|
1821
|
+
const fromCanvasId = nodeIdToCanvasId.get(edge.fromNodeId);
|
|
1822
|
+
const toCanvasId = nodeIdToCanvasId.get(edge.toNodeId);
|
|
1823
|
+
if (fromCanvasId && toCanvasId) {
|
|
1824
|
+
edges.push({
|
|
1825
|
+
id: `edge_${++edgeCounter}`,
|
|
1826
|
+
fromNode: fromCanvasId,
|
|
1827
|
+
toNode: toCanvasId,
|
|
1828
|
+
label: edge.alias || edge.relation,
|
|
1829
|
+
toEnd: "arrow"
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
const canvasData = { nodes, edges };
|
|
1834
|
+
atomicWriteFile(targetFilePath, JSON.stringify(canvasData, null, 2));
|
|
1835
|
+
return canvasData;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// db/index.ts
|
|
1839
|
+
class SynapseDB {
|
|
1840
|
+
engine;
|
|
1841
|
+
collections = new Map;
|
|
1842
|
+
constructor(config) {
|
|
1843
|
+
this.engine = new SynapseEngine(config);
|
|
1844
|
+
}
|
|
1845
|
+
async init() {
|
|
1846
|
+
await this.engine.init();
|
|
1847
|
+
}
|
|
1848
|
+
collection(name) {
|
|
1849
|
+
let col = this.collections.get(name);
|
|
1850
|
+
if (!col) {
|
|
1851
|
+
col = new SynapseCollection(name, this.engine);
|
|
1852
|
+
this.collections.set(name, col);
|
|
1853
|
+
}
|
|
1854
|
+
return col;
|
|
1855
|
+
}
|
|
1856
|
+
exportCanvas(targetFilePath, options) {
|
|
1857
|
+
return exportToObsidianCanvas(this.engine.connectome, targetFilePath, options);
|
|
1858
|
+
}
|
|
1859
|
+
createMigrator() {
|
|
1860
|
+
return new SynapseMigrator(this.engine);
|
|
1861
|
+
}
|
|
1862
|
+
stats() {
|
|
1863
|
+
return this.engine.getStats();
|
|
1864
|
+
}
|
|
1865
|
+
flush() {
|
|
1866
|
+
this.engine.flushPendingWrites();
|
|
1867
|
+
}
|
|
1868
|
+
async transaction(fn) {
|
|
1869
|
+
return this.engine.executeTransaction(fn);
|
|
1870
|
+
}
|
|
1871
|
+
asOfLsn(targetLsn) {
|
|
1872
|
+
return this.engine.wal.asOfLsn(this.engine.connectome, targetLsn);
|
|
1873
|
+
}
|
|
1874
|
+
asOf(timestampMsOrIso) {
|
|
1875
|
+
return this.engine.wal.asOf(this.engine.connectome, timestampMsOrIso);
|
|
1876
|
+
}
|
|
1877
|
+
diffBetweenLsn(fromLsn, toLsn) {
|
|
1878
|
+
return this.engine.wal.diffBetweenLsn(fromLsn, toLsn);
|
|
1879
|
+
}
|
|
1880
|
+
recordCompilerMetric(metric) {
|
|
1881
|
+
return this.engine.wal.recordCompilerMetric(metric);
|
|
1882
|
+
}
|
|
1883
|
+
walStats() {
|
|
1884
|
+
return this.engine.wal.getStats();
|
|
1885
|
+
}
|
|
1886
|
+
close() {
|
|
1887
|
+
this.engine.close();
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
export {
|
|
1891
|
+
CausalArenaAllocator,
|
|
1892
|
+
CausalTimeTravelEngine,
|
|
1893
|
+
CausalWalEngine,
|
|
1894
|
+
CausalWalEventType,
|
|
1895
|
+
SqliteCausalWalStore,
|
|
1896
|
+
SynapseCollection,
|
|
1897
|
+
SynapseConnectome,
|
|
1898
|
+
SynapseDB,
|
|
1899
|
+
SynapseEngine,
|
|
1900
|
+
SynapseMigrator,
|
|
1901
|
+
TemporalRingBuffer,
|
|
1902
|
+
WAL_CONFIG_DEFAULTS,
|
|
1903
|
+
atomicWriteFile,
|
|
1904
|
+
exportToObsidianCanvas,
|
|
1905
|
+
extractWikilinks,
|
|
1906
|
+
extractWikilinksFromDataAndBody,
|
|
1907
|
+
parseMarkdownFile,
|
|
1908
|
+
parseYamlScalar,
|
|
1909
|
+
stringifyDocument
|
|
1910
|
+
};
|