@runtimescope/collector 0.10.7 → 0.10.8

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.
@@ -76,6 +76,18 @@ var EventStore = class {
76
76
  this.sqliteStore = store;
77
77
  this.currentProject = project;
78
78
  }
79
+ /**
80
+ * Drop the SQLite pointer if it matches the given project. Called by the
81
+ * collector's idle-store eviction timer so the store doesn't try to write
82
+ * through a closed handle after eviction. The next `setSqliteStore` (from
83
+ * the next event for this project) restores the binding.
84
+ */
85
+ clearSqliteStoreIfMatches(project) {
86
+ if (this.currentProject === project) {
87
+ this.sqliteStore = null;
88
+ this.currentProject = null;
89
+ }
90
+ }
79
91
  /**
80
92
  * Pre-load recent events from a SqliteStore into the in-memory ring buffer.
81
93
  * Used at collector startup to make MCP tools immediately useful after a
@@ -1674,6 +1686,13 @@ var CollectorServer = class {
1674
1686
  pendingHandshakes = /* @__PURE__ */ new Set();
1675
1687
  pendingCommands = /* @__PURE__ */ new Map();
1676
1688
  sqliteStores = /* @__PURE__ */ new Map();
1689
+ // Per-project last-access timestamps for the SQLite stores. Used by the
1690
+ // LRU eviction timer to close handles for projects that haven't been read
1691
+ // from or written to recently — without this, every project ever seen
1692
+ // keeps its WAL handle + ~2-3MB page cache open forever, which on a
1693
+ // 40-project machine adds up to ~100MB of permanently-allocated baseline
1694
+ // RSS. ensureSqliteStore() updates this on every access.
1695
+ sqliteStoreLastAccess = /* @__PURE__ */ new Map();
1677
1696
  wals = /* @__PURE__ */ new Map();
1678
1697
  ready = false;
1679
1698
  metrics = new MetricsRegistry();
@@ -1684,6 +1703,7 @@ var CollectorServer = class {
1684
1703
  disconnectCallbacks = [];
1685
1704
  pruneTimer = null;
1686
1705
  heartbeatTimer = null;
1706
+ sqliteEvictTimer = null;
1687
1707
  tlsConfig = null;
1688
1708
  pmStore = null;
1689
1709
  constructor(options = {}) {
@@ -1873,8 +1893,55 @@ var CollectorServer = class {
1873
1893
  }
1874
1894
  }
1875
1895
  this.ready = true;
1896
+ this.startSqliteEvictionTimer();
1876
1897
  return this.tryStart(port, host, maxRetries, retryDelayMs, tls);
1877
1898
  }
1899
+ /**
1900
+ * Close SQLite stores that haven't been accessed in IDLE_TIMEOUT_MS. Any
1901
+ * subsequent `ensureSqliteStore(name)` call will transparently re-open the
1902
+ * store from disk, so eviction is invisible to callers — they just see a
1903
+ * brief warm-up the next time they touch a project.
1904
+ *
1905
+ * We deliberately do NOT evict stores belonging to currently-connected
1906
+ * SDK clients, since those are guaranteed to write again soon and closing
1907
+ * + reopening on every event would be expensive.
1908
+ */
1909
+ startSqliteEvictionTimer() {
1910
+ if (this.sqliteEvictTimer) return;
1911
+ const IDLE_TIMEOUT_MS = parseInt(
1912
+ process.env.RUNTIMESCOPE_SQLITE_IDLE_MS ?? String(5 * 60 * 1e3),
1913
+ 10
1914
+ );
1915
+ const SWEEP_INTERVAL_MS = parseInt(
1916
+ process.env.RUNTIMESCOPE_SQLITE_SWEEP_MS ?? String(60 * 1e3),
1917
+ 10
1918
+ );
1919
+ this.sqliteEvictTimer = setInterval(() => {
1920
+ const now = Date.now();
1921
+ const liveProjects = /* @__PURE__ */ new Set();
1922
+ for (const info of this.clients.values()) {
1923
+ liveProjects.add(info.projectName);
1924
+ }
1925
+ for (const [projectName, store] of this.sqliteStores) {
1926
+ if (liveProjects.has(projectName)) continue;
1927
+ const lastAccess = this.sqliteStoreLastAccess.get(projectName) ?? 0;
1928
+ if (now - lastAccess < IDLE_TIMEOUT_MS) continue;
1929
+ try {
1930
+ store.close();
1931
+ } catch (e) {
1932
+ console.error(
1933
+ `[RuntimeScope] Failed to close idle SQLite store "${projectName}":`,
1934
+ e.message
1935
+ );
1936
+ continue;
1937
+ }
1938
+ this.sqliteStores.delete(projectName);
1939
+ this.sqliteStoreLastAccess.delete(projectName);
1940
+ this.store.clearSqliteStoreIfMatches(projectName);
1941
+ }
1942
+ }, SWEEP_INTERVAL_MS);
1943
+ this.sqliteEvictTimer.unref?.();
1944
+ }
1878
1945
  /**
1879
1946
  * On collector startup, for each known project:
1880
1947
  * 1. Replay any sealed/active WAL files into SqliteStore (mirror of the
@@ -1979,6 +2046,7 @@ var CollectorServer = class {
1979
2046
  ensureSqliteStore(projectName) {
1980
2047
  if (!this.projectManager) return null;
1981
2048
  if (!isSqliteAvailable()) return null;
2049
+ this.sqliteStoreLastAccess.set(projectName, Date.now());
1982
2050
  let sqliteStore = this.sqliteStores.get(projectName);
1983
2051
  if (!sqliteStore) {
1984
2052
  try {
@@ -2361,6 +2429,10 @@ var CollectorServer = class {
2361
2429
  clearInterval(this.pruneTimer);
2362
2430
  this.pruneTimer = null;
2363
2431
  }
2432
+ if (this.sqliteEvictTimer) {
2433
+ clearInterval(this.sqliteEvictTimer);
2434
+ this.sqliteEvictTimer = null;
2435
+ }
2364
2436
  if (this.wss) {
2365
2437
  for (const client of this.wss.clients) {
2366
2438
  if (client.readyState === 1) {
@@ -7410,4 +7482,4 @@ export {
7410
7482
  parseSessionJsonl,
7411
7483
  ProjectDiscovery
7412
7484
  };
7413
- //# sourceMappingURL=chunk-ODBRN4NR.js.map
7485
+ //# sourceMappingURL=chunk-TT3VVKUE.js.map