@grafema/core 0.2.10 → 0.2.12-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/Orchestrator.d.ts +13 -0
  2. package/dist/Orchestrator.d.ts.map +1 -1
  3. package/dist/Orchestrator.js +84 -2
  4. package/dist/Orchestrator.js.map +1 -1
  5. package/dist/ParallelAnalysisRunner.d.ts.map +1 -1
  6. package/dist/ParallelAnalysisRunner.js +28 -41
  7. package/dist/ParallelAnalysisRunner.js.map +1 -1
  8. package/dist/PhaseRunner.d.ts +2 -0
  9. package/dist/PhaseRunner.d.ts.map +1 -1
  10. package/dist/PhaseRunner.js +8 -3
  11. package/dist/PhaseRunner.js.map +1 -1
  12. package/dist/core/IncrementalReanalyzer.d.ts +3 -3
  13. package/dist/core/IncrementalReanalyzer.d.ts.map +1 -1
  14. package/dist/core/IncrementalReanalyzer.js +12 -12
  15. package/dist/core/IncrementalReanalyzer.js.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/plugins/analysis/JSASTAnalyzer.d.ts.map +1 -1
  21. package/dist/plugins/analysis/JSASTAnalyzer.js +35 -3
  22. package/dist/plugins/analysis/JSASTAnalyzer.js.map +1 -1
  23. package/dist/plugins/analysis/ast/GraphBuilder.d.ts.map +1 -1
  24. package/dist/plugins/analysis/ast/GraphBuilder.js +8 -0
  25. package/dist/plugins/analysis/ast/GraphBuilder.js.map +1 -1
  26. package/dist/plugins/analysis/ast/builders/ModuleRuntimeBuilder.d.ts.map +1 -1
  27. package/dist/plugins/analysis/ast/builders/ModuleRuntimeBuilder.js +4 -2
  28. package/dist/plugins/analysis/ast/builders/ModuleRuntimeBuilder.js.map +1 -1
  29. package/dist/plugins/analysis/ast/handlers/NewExpressionHandler.d.ts.map +1 -1
  30. package/dist/plugins/analysis/ast/handlers/NewExpressionHandler.js +2 -1
  31. package/dist/plugins/analysis/ast/handlers/NewExpressionHandler.js.map +1 -1
  32. package/dist/plugins/analysis/ast/types.d.ts +3 -0
  33. package/dist/plugins/analysis/ast/types.d.ts.map +1 -1
  34. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts.map +1 -1
  35. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js +6 -2
  36. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js.map +1 -1
  37. package/dist/plugins/enrichment/ImportExportLinker.d.ts.map +1 -1
  38. package/dist/plugins/enrichment/ImportExportLinker.js +15 -5
  39. package/dist/plugins/enrichment/ImportExportLinker.js.map +1 -1
  40. package/dist/storage/backends/RFDBServerBackend.d.ts +21 -7
  41. package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -1
  42. package/dist/storage/backends/RFDBServerBackend.js +48 -48
  43. package/dist/storage/backends/RFDBServerBackend.js.map +1 -1
  44. package/dist/utils/startRfdbServer.d.ts +44 -0
  45. package/dist/utils/startRfdbServer.d.ts.map +1 -0
  46. package/dist/utils/startRfdbServer.js +79 -0
  47. package/dist/utils/startRfdbServer.js.map +1 -0
  48. package/package.json +3 -3
  49. package/src/Orchestrator.ts +91 -2
  50. package/src/ParallelAnalysisRunner.ts +30 -48
  51. package/src/PhaseRunner.ts +10 -3
  52. package/src/core/IncrementalReanalyzer.ts +15 -15
  53. package/src/index.ts +4 -0
  54. package/src/plugins/analysis/JSASTAnalyzer.ts +40 -3
  55. package/src/plugins/analysis/ast/GraphBuilder.ts +9 -0
  56. package/src/plugins/analysis/ast/builders/ModuleRuntimeBuilder.ts +4 -2
  57. package/src/plugins/analysis/ast/handlers/NewExpressionHandler.ts +2 -1
  58. package/src/plugins/analysis/ast/types.ts +3 -0
  59. package/src/plugins/analysis/ast/visitors/ImportExportVisitor.ts +8 -2
  60. package/src/plugins/enrichment/ImportExportLinker.ts +15 -5
  61. package/src/storage/backends/RFDBServerBackend.ts +52 -60
  62. package/src/utils/startRfdbServer.ts +126 -0
@@ -18,15 +18,14 @@
18
18
  * await backend.flush();
19
19
  */
20
20
 
21
- import { RFDBClient } from '@grafema/rfdb-client';
22
- import { existsSync, unlinkSync } from 'fs';
23
- import { spawn, type ChildProcess } from 'child_process';
21
+ import { RFDBClient, type BatchHandle } from '@grafema/rfdb-client';
22
+ import type { ChildProcess } from 'child_process';
24
23
  import { join, dirname } from 'path';
25
- import { setTimeout as sleep } from 'timers/promises';
26
24
 
27
25
  import type { WireNode, WireEdge, FieldDeclaration, CommitDelta, AttrQuery as RFDBAttrQuery } from '@grafema/types';
28
26
  import type { NodeType, EdgeType } from '@grafema/types';
29
- import { findRfdbBinary } from '../../utils/findRfdbBinary.js';
27
+ import { startRfdbServer } from '../../utils/startRfdbServer.js';
28
+ import { GRAFEMA_VERSION, getSchemaVersion } from '../../version.js';
30
29
  import type { BaseNodeRecord, EdgeRecord, AnyBrandedNode } from '@grafema/types';
31
30
  import { brandNodeInternal } from '../../core/brandNodeInternal.js';
32
31
  import type { AttrQuery, GraphStats, GraphExport } from '../../core/GraphBackend.js';
@@ -201,66 +200,19 @@ export class RFDBServerBackend {
201
200
  }
202
201
 
203
202
  /**
204
- * Find RFDB server binary using shared utility.
205
- * Delegates to findRfdbBinary() for consistent search across all entry points.
206
- */
207
- private _findServerBinary(): string | null {
208
- const binaryPath = findRfdbBinary();
209
- if (binaryPath) {
210
- this.log(`[RFDBServerBackend] Found binary: ${binaryPath}`);
211
- }
212
- return binaryPath;
213
- }
214
-
215
- /**
216
- * Start RFDB server process
203
+ * Start RFDB server process using shared utility.
217
204
  */
218
205
  private async _startServer(): Promise<void> {
219
206
  if (!this.dbPath) {
220
207
  throw new Error('dbPath required to start RFDB server');
221
208
  }
222
209
 
223
- // Find server binary - check multiple locations
224
- const binaryPath = this._findServerBinary();
225
- if (!binaryPath) {
226
- throw new Error(
227
- 'RFDB server binary not found.\n' +
228
- 'Install @grafema/rfdb: npm install @grafema/rfdb\n' +
229
- 'Or build from source: cargo build --release --bin rfdb-server'
230
- );
231
- }
232
-
233
- // Remove stale socket
234
- if (existsSync(this.socketPath)) {
235
- unlinkSync(this.socketPath);
236
- }
237
-
238
- this.log(`[RFDBServerBackend] Starting: ${binaryPath} ${this.dbPath} --socket ${this.socketPath}`);
239
-
240
- this.serverProcess = spawn(binaryPath, [this.dbPath, '--socket', this.socketPath], {
241
- stdio: ['ignore', 'ignore', 'inherit'], // stdin/stdout ignored, stderr inherited
242
- detached: true, // Allow server to outlive this process
243
- });
244
-
245
- // Don't let server process prevent parent from exiting
246
- this.serverProcess.unref();
247
-
248
- this.serverProcess.on('error', (err: Error) => {
249
- this.logError(`[RFDBServerBackend] Server process error:`, err);
210
+ await startRfdbServer({
211
+ dbPath: this.dbPath,
212
+ socketPath: this.socketPath,
213
+ waitTimeoutMs: 5000,
214
+ logger: this.silent ? undefined : { debug: (m: string) => this.log(m) },
250
215
  });
251
-
252
- // Wait for socket to appear
253
- let attempts = 0;
254
- while (!existsSync(this.socketPath) && attempts < 50) {
255
- await sleep(100);
256
- attempts++;
257
- }
258
-
259
- if (!existsSync(this.socketPath)) {
260
- throw new Error(`RFDB server failed to start (socket not created after ${attempts * 100}ms)`);
261
- }
262
-
263
- this.log(`[RFDBServerBackend] Server started on ${this.socketPath}`);
264
216
  }
265
217
 
266
218
  /**
@@ -274,9 +226,28 @@ export class RFDBServerBackend {
274
226
  try {
275
227
  const hello = await this.client.hello(3);
276
228
  this.protocolVersion = hello.protocolVersion;
229
+ this._checkServerVersion(hello.serverVersion);
277
230
  } catch {
278
231
  // Server predates hello command or doesn't support v3 — safe v2 fallback
279
232
  this.protocolVersion = 2;
233
+ this.log('[RFDBServerBackend] WARNING: Server does not support version negotiation. Consider updating rfdb-server.');
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Validate server version against expected client version.
239
+ * Warns on mismatch but never fails — version differences are informational.
240
+ */
241
+ private _checkServerVersion(serverVersion: string): void {
242
+ if (!serverVersion) return;
243
+ const expected = getSchemaVersion(GRAFEMA_VERSION);
244
+ const actual = getSchemaVersion(serverVersion);
245
+ if (actual !== expected) {
246
+ this.log(
247
+ `[RFDBServerBackend] WARNING: rfdb-server version mismatch — ` +
248
+ `server v${serverVersion}, expected v${GRAFEMA_VERSION}. ` +
249
+ `Update with: grafema server restart`
250
+ );
280
251
  }
281
252
  }
282
253
 
@@ -769,10 +740,13 @@ export class RFDBServerBackend {
769
740
  /**
770
741
  * Commit the current batch to the server atomically.
771
742
  * Returns a CommitDelta describing what changed.
743
+ *
744
+ * @param tags - Optional tags for the commit
745
+ * @param deferIndex - When true, server writes data but skips index rebuild.
772
746
  */
773
- async commitBatch(tags?: string[]): Promise<CommitDelta> {
747
+ async commitBatch(tags?: string[], deferIndex?: boolean, protectedTypes?: string[]): Promise<CommitDelta> {
774
748
  if (!this.client) throw new Error('Not connected to RFDB server');
775
- return this.client.commitBatch(tags);
749
+ return this.client.commitBatch(tags, deferIndex, protectedTypes);
776
750
  }
777
751
 
778
752
  /**
@@ -822,6 +796,24 @@ export class RFDBServerBackend {
822
796
  this.client.abortBatch();
823
797
  }
824
798
 
799
+ /**
800
+ * Rebuild all secondary indexes after deferred-index commits (REG-487).
801
+ * Call this once after a series of commitBatch(tags, true) calls.
802
+ */
803
+ async rebuildIndexes(): Promise<void> {
804
+ if (!this.client) throw new Error('Not connected to RFDB server');
805
+ await this.client.rebuildIndexes();
806
+ }
807
+
808
+ /**
809
+ * Create an isolated batch handle for concurrent-safe batching (REG-487).
810
+ * Each handle has its own buffers — safe for parallel workers.
811
+ */
812
+ createBatch(): BatchHandle {
813
+ if (!this.client) throw new Error('Not connected to RFDB server');
814
+ return this.client.createBatch();
815
+ }
816
+
825
817
  // ===========================================================================
826
818
  // Export/Import
827
819
  // ===========================================================================
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Shared utility for starting rfdb-server
3
+ *
4
+ * Single authoritative function for spawning rfdb-server. All spawn sites
5
+ * (RFDBServerBackend, CLI server command, ParallelAnalysisRunner) delegate here.
6
+ *
7
+ * Callers are responsible for checking if a server is already running before
8
+ * calling this function. This function always spawns a new server process.
9
+ */
10
+
11
+ import { existsSync, unlinkSync, writeFileSync } from 'fs';
12
+ import { dirname } from 'path';
13
+ import { spawn, type ChildProcess } from 'child_process';
14
+ import { setTimeout as sleep } from 'timers/promises';
15
+ import { findRfdbBinary } from './findRfdbBinary.js';
16
+
17
+ export interface StartRfdbServerOptions {
18
+ dbPath: string;
19
+ socketPath: string;
20
+ /** Override binary path; if absent, findRfdbBinary() is called */
21
+ binaryPath?: string;
22
+ /** If provided, PID file is written after spawn */
23
+ pidPath?: string;
24
+ /** Socket poll timeout in ms (default: 5000) */
25
+ waitTimeoutMs?: number;
26
+ /** Optional logger for debug messages */
27
+ logger?: { debug(msg: string): void };
28
+ /** Internal: dependency injection for testing */
29
+ _deps?: {
30
+ spawn?: typeof spawn;
31
+ findRfdbBinary?: () => string | null;
32
+ existsSync?: (path: string) => boolean;
33
+ unlinkSync?: (path: string) => void;
34
+ writeFileSync?: (path: string, data: string) => void;
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Start an rfdb-server process.
40
+ *
41
+ * 1. Resolve binary (explicit or via findRfdbBinary)
42
+ * 2. Remove stale socket
43
+ * 3. Spawn detached process
44
+ * 4. Write PID file (if pidPath provided)
45
+ * 5. Poll for socket file up to waitTimeoutMs
46
+ * 6. Return ChildProcess (caller decides whether to kill later)
47
+ */
48
+ export async function startRfdbServer(options: StartRfdbServerOptions): Promise<ChildProcess> {
49
+ const {
50
+ dbPath,
51
+ socketPath,
52
+ pidPath,
53
+ waitTimeoutMs = 5000,
54
+ logger,
55
+ _deps,
56
+ } = options;
57
+
58
+ const _spawn = _deps?.spawn ?? spawn;
59
+ const _findRfdbBinary = _deps?.findRfdbBinary ?? findRfdbBinary;
60
+ const _existsSync = _deps?.existsSync ?? existsSync;
61
+ const _unlinkSync = _deps?.unlinkSync ?? unlinkSync;
62
+ const _writeFileSync = _deps?.writeFileSync ?? writeFileSync;
63
+
64
+ // 1. Resolve binary
65
+ const binaryPath = options.binaryPath || _findRfdbBinary();
66
+ if (!binaryPath) {
67
+ throw new Error(
68
+ 'RFDB server binary not found.\n' +
69
+ 'Install @grafema/rfdb: npm install @grafema/rfdb\n' +
70
+ 'Or build from source: cargo build --release --bin rfdb-server'
71
+ );
72
+ }
73
+
74
+ // 2. Remove stale socket
75
+ if (_existsSync(socketPath)) {
76
+ _unlinkSync(socketPath);
77
+ }
78
+
79
+ const dataDir = dirname(socketPath);
80
+ logger?.debug(`Starting rfdb-server: ${binaryPath} ${dbPath} --socket ${socketPath} --data-dir ${dataDir}`);
81
+
82
+ // 3. Spawn server (detached, survives parent exit)
83
+ // Mutable container to capture async spawn errors (Dijkstra amendment B)
84
+ const state = { spawnError: null as Error | null };
85
+
86
+ const serverProcess = _spawn(binaryPath, [dbPath, '--socket', socketPath, '--data-dir', dataDir], {
87
+ stdio: ['ignore', 'ignore', 'inherit'],
88
+ detached: true,
89
+ });
90
+
91
+ serverProcess.unref();
92
+
93
+ // Wire error handler to capture ENOENT and other spawn failures
94
+ serverProcess.on('error', (err: Error) => {
95
+ state.spawnError = err;
96
+ });
97
+
98
+ // 4. Write PID file if requested and pid is available
99
+ if (pidPath && serverProcess.pid) {
100
+ _writeFileSync(pidPath, String(serverProcess.pid));
101
+ }
102
+
103
+ // 5. Poll for socket file
104
+ const maxAttempts = Math.ceil(waitTimeoutMs / 100);
105
+ let attempts = 0;
106
+ while (!_existsSync(socketPath) && attempts < maxAttempts) {
107
+ if (state.spawnError) {
108
+ throw new Error(
109
+ `RFDB server failed to start: ${state.spawnError.message} — check binary: ${binaryPath}`
110
+ );
111
+ }
112
+ await sleep(100);
113
+ attempts++;
114
+ }
115
+
116
+ // 6. Final check
117
+ if (!_existsSync(socketPath)) {
118
+ const detail = state.spawnError ? `: ${state.spawnError.message}` : '';
119
+ throw new Error(
120
+ `RFDB server failed to start after ${waitTimeoutMs}ms${detail} — check binary: ${binaryPath}`
121
+ );
122
+ }
123
+
124
+ logger?.debug(`rfdb-server started on ${socketPath}`);
125
+ return serverProcess;
126
+ }