@lapis-notes/graph 0.1.0
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/CHANGELOG.md +6 -0
- package/LICENSE.md +8 -0
- package/README.md +20 -0
- package/dist/embed.d.ts +5 -0
- package/dist/embed.js +4 -0
- package/dist/graph-build-generation.d.ts +6 -0
- package/dist/graph-build-generation.js +13 -0
- package/dist/graph-controls-overlay.svelte +840 -0
- package/dist/graph-controls-overlay.svelte.d.ts +30 -0
- package/dist/graph-data-coordinator.d.ts +41 -0
- package/dist/graph-data-coordinator.js +265 -0
- package/dist/graph-data.d.ts +23 -0
- package/dist/graph-data.js +493 -0
- package/dist/graph-embed.svelte +92 -0
- package/dist/graph-embed.svelte.d.ts +12 -0
- package/dist/graph-file-preview.svelte +115 -0
- package/dist/graph-file-preview.svelte.d.ts +12 -0
- package/dist/graph-hover-intent.d.ts +22 -0
- package/dist/graph-hover-intent.js +85 -0
- package/dist/graph-load-alignment.d.ts +2 -0
- package/dist/graph-load-alignment.js +3 -0
- package/dist/graph-navigation.d.ts +7 -0
- package/dist/graph-navigation.js +78 -0
- package/dist/graph-node-activation.d.ts +4 -0
- package/dist/graph-node-activation.js +16 -0
- package/dist/graph-plugin.d.ts +38 -0
- package/dist/graph-plugin.js +177 -0
- package/dist/graph-problems.d.ts +15 -0
- package/dist/graph-problems.js +46 -0
- package/dist/graph-query-resolution.d.ts +9 -0
- package/dist/graph-query-resolution.js +44 -0
- package/dist/graph-renderer.d.ts +226 -0
- package/dist/graph-renderer.js +1350 -0
- package/dist/graph-settings.d.ts +20 -0
- package/dist/graph-settings.js +125 -0
- package/dist/graph-types.d.ts +74 -0
- package/dist/graph-types.js +1 -0
- package/dist/graph-view.d.ts +62 -0
- package/dist/graph-view.js +553 -0
- package/dist/graph-viewport-alignment.d.ts +6 -0
- package/dist/graph-viewport-alignment.js +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/mount-local-component.svelte.d.ts +7 -0
- package/dist/mount-local-component.svelte.js +12 -0
- package/dist/public-components.d.ts +2 -0
- package/dist/public-components.js +2 -0
- package/dist/styles.css +441 -0
- package/manifest.json +30 -0
- package/package.json +84 -0
- package/styles.css +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type App } from "@lapis-notes/api";
|
|
2
|
+
import { type SearchFilterSyntax } from "@lapismd/design-core/filter";
|
|
3
|
+
import type { GraphNodePreview } from "./graph-renderer";
|
|
4
|
+
import type { GraphSettings, GraphSettingsPatch } from "./graph-types";
|
|
5
|
+
type $$ComponentProps = {
|
|
6
|
+
app: App;
|
|
7
|
+
isLocal: boolean;
|
|
8
|
+
settings: GraphSettings;
|
|
9
|
+
statsText: string;
|
|
10
|
+
statusText: string;
|
|
11
|
+
statusKind: "loading" | "error" | null;
|
|
12
|
+
groupDiagnostics: Readonly<Record<string, string>>;
|
|
13
|
+
filterDiagnostic: string | null;
|
|
14
|
+
filterSyntax: SearchFilterSyntax;
|
|
15
|
+
preview: GraphNodePreview | null;
|
|
16
|
+
isAnimating: boolean;
|
|
17
|
+
onFocusActiveFile: () => void;
|
|
18
|
+
onZoomIn: () => void;
|
|
19
|
+
onZoomOut: () => void;
|
|
20
|
+
onResetView: () => void;
|
|
21
|
+
onRefreshGraph: () => void;
|
|
22
|
+
onResetDefaults: () => void;
|
|
23
|
+
onToggleAnimation: () => void;
|
|
24
|
+
onSettingsPatch: (patch: GraphSettingsPatch) => void;
|
|
25
|
+
onOpenPreviewFile: (preview: GraphNodePreview, event: MouseEvent) => void;
|
|
26
|
+
onDismissPreview: () => void;
|
|
27
|
+
};
|
|
28
|
+
declare const GraphControlsOverlay: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
29
|
+
type GraphControlsOverlay = ReturnType<typeof GraphControlsOverlay>;
|
|
30
|
+
export default GraphControlsOverlay;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { App } from "@lapis-notes/api";
|
|
2
|
+
import { type GraphBuildProgress } from "./graph-data";
|
|
3
|
+
import type { GraphData } from "./graph-types";
|
|
4
|
+
export type GraphCoordinatorStatus = "idle" | "loading" | "updating" | "ready" | "error";
|
|
5
|
+
export type GraphSnapshotOutcome = "none" | "hit" | "stale" | "corrupt";
|
|
6
|
+
export interface GraphCoordinatorState {
|
|
7
|
+
version: number;
|
|
8
|
+
graph: GraphData | null;
|
|
9
|
+
status: GraphCoordinatorStatus;
|
|
10
|
+
progress: GraphBuildProgress | null;
|
|
11
|
+
error: string | null;
|
|
12
|
+
snapshotOutcome: GraphSnapshotOutcome;
|
|
13
|
+
completedAt: number | null;
|
|
14
|
+
}
|
|
15
|
+
type GraphCoordinatorApp = Pick<App, "appDatabase" | "metadataCache" | "telemetry" | "vault" | "workspace">;
|
|
16
|
+
export declare function cloneGraphData(graph: GraphData): GraphData;
|
|
17
|
+
export declare class GraphDataCoordinator {
|
|
18
|
+
private readonly app;
|
|
19
|
+
private readonly listeners;
|
|
20
|
+
private state;
|
|
21
|
+
private startPromise;
|
|
22
|
+
private runPromise;
|
|
23
|
+
private pending;
|
|
24
|
+
private pendingForce;
|
|
25
|
+
private disposed;
|
|
26
|
+
private abortController;
|
|
27
|
+
private currentFingerprint;
|
|
28
|
+
constructor(app: GraphCoordinatorApp);
|
|
29
|
+
subscribe(listener: (state: GraphCoordinatorState) => void): () => void;
|
|
30
|
+
getState(): GraphCoordinatorState;
|
|
31
|
+
start(): Promise<void>;
|
|
32
|
+
requestRefresh(_reason?: string, force?: boolean): Promise<void>;
|
|
33
|
+
dispose(): void;
|
|
34
|
+
private performStart;
|
|
35
|
+
private runQueue;
|
|
36
|
+
private buildOnce;
|
|
37
|
+
private handleFailure;
|
|
38
|
+
private updateState;
|
|
39
|
+
}
|
|
40
|
+
export declare const GRAPH_CANONICAL_SNAPSHOT_KEY = "graph.canonical-snapshot.v1";
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { buildCanonicalGraph, } from "./graph-data";
|
|
2
|
+
const GRAPH_SNAPSHOT_KEY = "graph.canonical-snapshot.v1";
|
|
3
|
+
const GRAPH_SNAPSHOT_VERSION = 1;
|
|
4
|
+
function cloneNode(node) {
|
|
5
|
+
return {
|
|
6
|
+
...node,
|
|
7
|
+
tags: [...node.tags],
|
|
8
|
+
groupIds: [...node.groupIds],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function cloneLink(link) {
|
|
12
|
+
return { ...link };
|
|
13
|
+
}
|
|
14
|
+
export function cloneGraphData(graph) {
|
|
15
|
+
return {
|
|
16
|
+
nodes: graph.nodes.map(cloneNode),
|
|
17
|
+
links: graph.links.map(cloneLink),
|
|
18
|
+
centerNodeId: graph.centerNodeId ?? null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function isGraphData(value) {
|
|
22
|
+
if (!value || typeof value !== "object")
|
|
23
|
+
return false;
|
|
24
|
+
const candidate = value;
|
|
25
|
+
return (Array.isArray(candidate.nodes) &&
|
|
26
|
+
candidate.nodes.every((node) => node &&
|
|
27
|
+
typeof node === "object" &&
|
|
28
|
+
typeof node.id === "string" &&
|
|
29
|
+
typeof node.label === "string" &&
|
|
30
|
+
Array.isArray(node.tags) &&
|
|
31
|
+
Array.isArray(node.groupIds)) &&
|
|
32
|
+
Array.isArray(candidate.links) &&
|
|
33
|
+
candidate.links.every((link) => link &&
|
|
34
|
+
typeof link === "object" &&
|
|
35
|
+
typeof link.id === "string" &&
|
|
36
|
+
typeof link.source === "string" &&
|
|
37
|
+
typeof link.target === "string"));
|
|
38
|
+
}
|
|
39
|
+
function parseSnapshot(value) {
|
|
40
|
+
if (!value || typeof value !== "object")
|
|
41
|
+
return null;
|
|
42
|
+
const candidate = value;
|
|
43
|
+
if (candidate.version !== GRAPH_SNAPSHOT_VERSION ||
|
|
44
|
+
typeof candidate.completedAt !== "number" ||
|
|
45
|
+
typeof candidate.metadataFingerprint !== "string" ||
|
|
46
|
+
!isGraphData(candidate.graph)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
version: GRAPH_SNAPSHOT_VERSION,
|
|
51
|
+
completedAt: candidate.completedAt,
|
|
52
|
+
metadataFingerprint: candidate.metadataFingerprint,
|
|
53
|
+
graph: cloneGraphData(candidate.graph),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function isCancellation(error) {
|
|
57
|
+
return error instanceof Error && error.name === "AbortError";
|
|
58
|
+
}
|
|
59
|
+
export class GraphDataCoordinator {
|
|
60
|
+
app;
|
|
61
|
+
listeners = new Set();
|
|
62
|
+
state = {
|
|
63
|
+
version: 0,
|
|
64
|
+
graph: null,
|
|
65
|
+
status: "idle",
|
|
66
|
+
progress: null,
|
|
67
|
+
error: null,
|
|
68
|
+
snapshotOutcome: "none",
|
|
69
|
+
completedAt: null,
|
|
70
|
+
};
|
|
71
|
+
startPromise = null;
|
|
72
|
+
runPromise = null;
|
|
73
|
+
pending = false;
|
|
74
|
+
pendingForce = false;
|
|
75
|
+
disposed = false;
|
|
76
|
+
abortController = null;
|
|
77
|
+
currentFingerprint = null;
|
|
78
|
+
constructor(app) {
|
|
79
|
+
this.app = app;
|
|
80
|
+
}
|
|
81
|
+
subscribe(listener) {
|
|
82
|
+
this.listeners.add(listener);
|
|
83
|
+
listener(this.getState());
|
|
84
|
+
return () => this.listeners.delete(listener);
|
|
85
|
+
}
|
|
86
|
+
getState() {
|
|
87
|
+
return {
|
|
88
|
+
...this.state,
|
|
89
|
+
graph: this.state.graph ? cloneGraphData(this.state.graph) : null,
|
|
90
|
+
progress: this.state.progress ? { ...this.state.progress } : null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
start() {
|
|
94
|
+
this.startPromise ??= this.performStart();
|
|
95
|
+
return this.startPromise;
|
|
96
|
+
}
|
|
97
|
+
requestRefresh(_reason = "refresh", force = false) {
|
|
98
|
+
if (this.disposed)
|
|
99
|
+
return Promise.resolve();
|
|
100
|
+
this.pending = true;
|
|
101
|
+
this.pendingForce ||= force;
|
|
102
|
+
this.runPromise ??= this.runQueue().finally(() => {
|
|
103
|
+
this.runPromise = null;
|
|
104
|
+
});
|
|
105
|
+
return this.runPromise;
|
|
106
|
+
}
|
|
107
|
+
dispose() {
|
|
108
|
+
this.disposed = true;
|
|
109
|
+
this.pending = false;
|
|
110
|
+
this.pendingForce = false;
|
|
111
|
+
this.abortController?.abort();
|
|
112
|
+
this.abortController = null;
|
|
113
|
+
this.listeners.clear();
|
|
114
|
+
}
|
|
115
|
+
async performStart() {
|
|
116
|
+
try {
|
|
117
|
+
await this.app.appDatabase.open();
|
|
118
|
+
if (this.disposed)
|
|
119
|
+
return;
|
|
120
|
+
const stored = await this.app.appDatabase.getMeta(GRAPH_SNAPSHOT_KEY);
|
|
121
|
+
const snapshot = parseSnapshot(stored);
|
|
122
|
+
const fingerprint = this.app.metadataCache.getReconciliationFingerprint();
|
|
123
|
+
if (snapshot) {
|
|
124
|
+
const matching = snapshot.metadataFingerprint === fingerprint;
|
|
125
|
+
this.currentFingerprint = snapshot.metadataFingerprint;
|
|
126
|
+
this.updateState({
|
|
127
|
+
graph: snapshot.graph,
|
|
128
|
+
status: matching ? "ready" : "updating",
|
|
129
|
+
progress: null,
|
|
130
|
+
error: null,
|
|
131
|
+
snapshotOutcome: matching ? "hit" : "stale",
|
|
132
|
+
completedAt: snapshot.completedAt,
|
|
133
|
+
});
|
|
134
|
+
this.app.telemetry.recordEvent("graph.snapshot", {
|
|
135
|
+
outcome: matching ? "hit" : "stale",
|
|
136
|
+
"graph.node.count": snapshot.graph.nodes.length,
|
|
137
|
+
"graph.link.count": snapshot.graph.links.length,
|
|
138
|
+
});
|
|
139
|
+
if (matching)
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const outcome = stored === undefined ? "none" : "corrupt";
|
|
144
|
+
this.updateState({
|
|
145
|
+
status: "loading",
|
|
146
|
+
snapshotOutcome: outcome,
|
|
147
|
+
error: null,
|
|
148
|
+
});
|
|
149
|
+
this.app.telemetry.recordEvent("graph.snapshot", { outcome });
|
|
150
|
+
}
|
|
151
|
+
void this.app.metadataCache.load().then(() => {
|
|
152
|
+
if (!this.disposed)
|
|
153
|
+
void this.requestRefresh("startup");
|
|
154
|
+
}, (error) => {
|
|
155
|
+
if (!this.disposed)
|
|
156
|
+
this.handleFailure(error);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
this.handleFailure(error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async runQueue() {
|
|
164
|
+
await this.start();
|
|
165
|
+
await this.app.metadataCache.load();
|
|
166
|
+
while (this.pending && !this.disposed) {
|
|
167
|
+
const force = this.pendingForce;
|
|
168
|
+
this.pending = false;
|
|
169
|
+
this.pendingForce = false;
|
|
170
|
+
await this.buildOnce(force);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async buildOnce(force) {
|
|
174
|
+
const fingerprint = this.app.metadataCache.getReconciliationFingerprint();
|
|
175
|
+
if (!force && this.state.graph && this.currentFingerprint === fingerprint) {
|
|
176
|
+
this.updateState({ status: "ready", progress: null, error: null });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.updateState({
|
|
180
|
+
status: this.state.graph ? "updating" : "loading",
|
|
181
|
+
progress: { processed: 0, total: 0, pages: 0 },
|
|
182
|
+
error: null,
|
|
183
|
+
});
|
|
184
|
+
this.abortController = new AbortController();
|
|
185
|
+
const startedAt = performance.now();
|
|
186
|
+
let lastProgress = {
|
|
187
|
+
processed: 0,
|
|
188
|
+
total: 0,
|
|
189
|
+
pages: 0,
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
const graph = await this.app.telemetry.measureAsync("graph.build", async (span) => {
|
|
193
|
+
const result = await buildCanonicalGraph(this.app, {
|
|
194
|
+
signal: this.abortController?.signal,
|
|
195
|
+
onProgress: (progress) => {
|
|
196
|
+
lastProgress = progress;
|
|
197
|
+
span.setAttribute("graph.files.processed", progress.processed);
|
|
198
|
+
span.setAttribute("graph.files.total", progress.total);
|
|
199
|
+
span.setAttribute("graph.page.count", progress.pages);
|
|
200
|
+
this.updateState({ progress });
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
span.setAttribute("graph.node.count", result.nodes.length);
|
|
204
|
+
span.setAttribute("graph.link.count", result.links.length);
|
|
205
|
+
return result;
|
|
206
|
+
});
|
|
207
|
+
if (this.disposed)
|
|
208
|
+
return;
|
|
209
|
+
const completedFingerprint = this.app.metadataCache.getReconciliationFingerprint();
|
|
210
|
+
const completedAt = Date.now();
|
|
211
|
+
this.currentFingerprint =
|
|
212
|
+
fingerprint === completedFingerprint ? completedFingerprint : null;
|
|
213
|
+
this.updateState({
|
|
214
|
+
graph,
|
|
215
|
+
status: fingerprint === completedFingerprint ? "ready" : "updating",
|
|
216
|
+
progress: null,
|
|
217
|
+
error: null,
|
|
218
|
+
completedAt,
|
|
219
|
+
});
|
|
220
|
+
if (fingerprint === completedFingerprint) {
|
|
221
|
+
await this.app.appDatabase.setMeta(GRAPH_SNAPSHOT_KEY, {
|
|
222
|
+
version: GRAPH_SNAPSHOT_VERSION,
|
|
223
|
+
completedAt,
|
|
224
|
+
metadataFingerprint: completedFingerprint,
|
|
225
|
+
graph,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
this.pending = true;
|
|
230
|
+
}
|
|
231
|
+
this.app.telemetry.recordEvent("graph.build.complete", {
|
|
232
|
+
"graph.node.count": graph.nodes.length,
|
|
233
|
+
"graph.link.count": graph.links.length,
|
|
234
|
+
"graph.page.count": lastProgress.pages,
|
|
235
|
+
"graph.duration.ms": Math.round(performance.now() - startedAt),
|
|
236
|
+
stable: fingerprint === completedFingerprint,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (!isCancellation(error))
|
|
241
|
+
this.handleFailure(error);
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
this.abortController = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
handleFailure(error) {
|
|
248
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
249
|
+
this.updateState({ status: "error", progress: null, error: message });
|
|
250
|
+
this.app.telemetry.recordEvent("graph.build.failed", {
|
|
251
|
+
cached: Boolean(this.state.graph),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
updateState(patch) {
|
|
255
|
+
this.state = {
|
|
256
|
+
...this.state,
|
|
257
|
+
...patch,
|
|
258
|
+
version: "graph" in patch ? this.state.version + 1 : this.state.version,
|
|
259
|
+
};
|
|
260
|
+
const snapshot = this.getState();
|
|
261
|
+
for (const listener of this.listeners)
|
|
262
|
+
listener(snapshot);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
export const GRAPH_CANONICAL_SNAPSHOT_KEY = GRAPH_SNAPSHOT_KEY;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { App, TFile } from "@lapis-notes/api";
|
|
2
|
+
import type { GraphData, GraphSettings } from "./graph-types";
|
|
3
|
+
type GraphApp = Pick<App, "vault" | "metadataCache" | "workspace">;
|
|
4
|
+
export interface GraphBuildProgress {
|
|
5
|
+
processed: number;
|
|
6
|
+
total: number;
|
|
7
|
+
pages: number;
|
|
8
|
+
}
|
|
9
|
+
export interface GraphBuildOptions {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
onProgress?: (progress: GraphBuildProgress) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function normalizeGraphTag(tag: string): string;
|
|
14
|
+
export declare function filterGraphBySettings(graph: GraphData, settings: GraphSettings, matches?: {
|
|
15
|
+
filterPaths?: ReadonlySet<string>;
|
|
16
|
+
groupPaths?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
17
|
+
}): GraphData;
|
|
18
|
+
export declare function buildCanonicalGraph(app: GraphApp, options?: GraphBuildOptions): Promise<GraphData>;
|
|
19
|
+
export declare function buildGlobalGraph(app: GraphApp, settings: GraphSettings): Promise<GraphData>;
|
|
20
|
+
export declare function buildLocalGraph(app: GraphApp, settings: GraphSettings, activeFile: TFile | null): Promise<GraphData>;
|
|
21
|
+
export declare function buildCanonicalLocalGraph(app: GraphApp, settings: GraphSettings, activeFile: TFile | null): Promise<GraphData>;
|
|
22
|
+
export declare function graphNodeIdForFile(path: string): string;
|
|
23
|
+
export {};
|