@runtimescope/collector 0.7.1 → 0.7.2

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.
@@ -297,40 +297,84 @@ var EventStore = class {
297
297
 
298
298
  // src/sqlite-store.ts
299
299
  import Database from "better-sqlite3";
300
- var SqliteStore = class {
300
+ import { renameSync, existsSync } from "fs";
301
+ var SqliteStore = class _SqliteStore {
301
302
  db;
302
303
  writeBuffer = [];
303
304
  flushTimer = null;
304
305
  batchSize;
306
+ dbPath;
307
+ static MAX_SNAPSHOTS_PER_SESSION = 50;
305
308
  insertEventStmt;
306
309
  insertSessionStmt;
307
310
  updateSessionDisconnectedStmt;
308
311
  constructor(options) {
309
- this.db = new Database(options.dbPath);
312
+ this.dbPath = options.dbPath;
310
313
  this.batchSize = options.batchSize ?? 50;
311
- if (options.walMode !== false) {
312
- this.db.pragma("journal_mode = WAL");
314
+ this.db = this.openDatabase(options);
315
+ const flushInterval = options.flushIntervalMs ?? 100;
316
+ this.flushTimer = setInterval(() => this.flush(), flushInterval);
317
+ }
318
+ openDatabase(options) {
319
+ try {
320
+ const db = new Database(options.dbPath);
321
+ if (options.walMode !== false) {
322
+ db.pragma("journal_mode = WAL");
323
+ }
324
+ db.pragma("synchronous = NORMAL");
325
+ const check = db.pragma("integrity_check");
326
+ if (check[0]?.integrity_check !== "ok") {
327
+ throw new Error("Integrity check failed");
328
+ }
329
+ this.createSchema(db);
330
+ this.prepareStatements(db);
331
+ return db;
332
+ } catch (err) {
333
+ console.error(
334
+ `[RuntimeScope] SQLite database corrupt or unreadable (${err.message}), recreating...`
335
+ );
336
+ try {
337
+ if (existsSync(options.dbPath)) {
338
+ const backupPath = `${options.dbPath}.corrupt.${Date.now()}`;
339
+ renameSync(options.dbPath, backupPath);
340
+ console.error(`[RuntimeScope] Renamed corrupt DB to ${backupPath}`);
341
+ }
342
+ for (const suffix of ["-wal", "-shm"]) {
343
+ const p = options.dbPath + suffix;
344
+ if (existsSync(p)) {
345
+ renameSync(p, `${p}.corrupt.${Date.now()}`);
346
+ }
347
+ }
348
+ } catch {
349
+ }
350
+ const db = new Database(options.dbPath);
351
+ if (options.walMode !== false) {
352
+ db.pragma("journal_mode = WAL");
353
+ }
354
+ db.pragma("synchronous = NORMAL");
355
+ this.createSchema(db);
356
+ this.prepareStatements(db);
357
+ return db;
313
358
  }
314
- this.db.pragma("synchronous = NORMAL");
315
- this.createSchema();
316
- this.insertEventStmt = this.db.prepare(`
359
+ }
360
+ prepareStatements(db) {
361
+ this.insertEventStmt = db.prepare(`
317
362
  INSERT INTO events (event_id, session_id, project, event_type, timestamp, data)
318
363
  VALUES (?, ?, ?, ?, ?, ?)
319
364
  `);
320
- this.insertSessionStmt = this.db.prepare(`
365
+ this.insertSessionStmt = db.prepare(`
321
366
  INSERT OR REPLACE INTO sessions (
322
367
  session_id, project, app_name, connected_at, sdk_version,
323
368
  event_count, is_connected, build_meta
324
369
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
325
370
  `);
326
- this.updateSessionDisconnectedStmt = this.db.prepare(`
371
+ this.updateSessionDisconnectedStmt = db.prepare(`
327
372
  UPDATE sessions SET is_connected = 0, disconnected_at = ? WHERE session_id = ?
328
373
  `);
329
- const flushInterval = options.flushIntervalMs ?? 100;
330
- this.flushTimer = setInterval(() => this.flush(), flushInterval);
331
374
  }
332
- createSchema() {
333
- this.db.exec(`
375
+ createSchema(db) {
376
+ const d = db ?? this.db;
377
+ d.exec(`
334
378
  CREATE TABLE IF NOT EXISTS events (
335
379
  id INTEGER PRIMARY KEY AUTOINCREMENT,
336
380
  event_id TEXT NOT NULL UNIQUE,
@@ -374,7 +418,7 @@ var SqliteStore = class {
374
418
  CREATE INDEX IF NOT EXISTS idx_snapshots_session ON session_snapshots(session_id);
375
419
  CREATE INDEX IF NOT EXISTS idx_snapshots_project ON session_snapshots(project, created_at);
376
420
  `);
377
- this.migrateSessionMetrics();
421
+ this.migrateSessionMetrics(d);
378
422
  }
379
423
  // --- Write Operations ---
380
424
  addEvent(event, project) {
@@ -427,6 +471,21 @@ var SqliteStore = class {
427
471
  INSERT INTO session_snapshots (session_id, project, label, metrics, created_at)
428
472
  VALUES (?, ?, ?, ?, ?)
429
473
  `).run(sessionId, project, label ?? null, JSON.stringify(metrics), Date.now());
474
+ this.pruneSnapshots(sessionId);
475
+ }
476
+ /** Remove oldest snapshots for a session beyond the retention limit */
477
+ pruneSnapshots(sessionId) {
478
+ const count = this.db.prepare("SELECT COUNT(*) as cnt FROM session_snapshots WHERE session_id = ?").get(sessionId).cnt;
479
+ if (count > _SqliteStore.MAX_SNAPSHOTS_PER_SESSION) {
480
+ this.db.prepare(`
481
+ DELETE FROM session_snapshots WHERE id IN (
482
+ SELECT id FROM session_snapshots
483
+ WHERE session_id = ?
484
+ ORDER BY created_at ASC
485
+ LIMIT ?
486
+ )
487
+ `).run(sessionId, count - _SqliteStore.MAX_SNAPSHOTS_PER_SESSION);
488
+ }
430
489
  }
431
490
  // --- Read Operations ---
432
491
  getEvents(filter) {
@@ -552,17 +611,17 @@ var SqliteStore = class {
552
611
  return rows.map((row) => JSON.parse(row.data));
553
612
  }
554
613
  // --- Migration ---
555
- migrateSessionMetrics() {
556
- const hasOldTable = this.db.prepare(
614
+ migrateSessionMetrics(db) {
615
+ const hasOldTable = db.prepare(
557
616
  "SELECT name FROM sqlite_master WHERE type='table' AND name='session_metrics'"
558
617
  ).get();
559
618
  if (hasOldTable) {
560
- this.db.exec(`
619
+ db.exec(`
561
620
  INSERT OR IGNORE INTO session_snapshots (session_id, project, label, metrics, created_at)
562
621
  SELECT session_id, project, 'auto-disconnect', metrics, created_at
563
622
  FROM session_metrics
564
623
  `);
565
- this.db.exec("DROP TABLE session_metrics");
624
+ db.exec("DROP TABLE session_metrics");
566
625
  }
567
626
  }
568
627
  // --- Maintenance ---
@@ -727,6 +786,7 @@ var CollectorServer = class {
727
786
  connectCallbacks = [];
728
787
  disconnectCallbacks = [];
729
788
  pruneTimer = null;
789
+ heartbeatTimer = null;
730
790
  tlsConfig = null;
731
791
  constructor(options = {}) {
732
792
  this.store = new EventStore(options.bufferSize ?? 1e4);
@@ -786,6 +846,8 @@ var CollectorServer = class {
786
846
  httpsServer.on("listening", () => {
787
847
  this.wss = wss;
788
848
  this.setupConnectionHandler(wss);
849
+ this.setupPersistentErrorHandler(wss);
850
+ this.startHeartbeat(wss);
789
851
  console.error(`[RuntimeScope] Collector listening on wss://${host}:${port}`);
790
852
  resolve2();
791
853
  });
@@ -799,6 +861,8 @@ var CollectorServer = class {
799
861
  wss.on("listening", () => {
800
862
  this.wss = wss;
801
863
  this.setupConnectionHandler(wss);
864
+ this.setupPersistentErrorHandler(wss);
865
+ this.startHeartbeat(wss);
802
866
  console.error(`[RuntimeScope] Collector listening on ws://${host}:${port}`);
803
867
  resolve2();
804
868
  });
@@ -844,8 +908,33 @@ var CollectorServer = class {
844
908
  }
845
909
  return sqliteStore;
846
910
  }
911
+ /** Catch runtime errors on the WSS so an unhandled error doesn't crash the process */
912
+ setupPersistentErrorHandler(wss) {
913
+ wss.on("error", (err) => {
914
+ console.error("[RuntimeScope] WebSocket server runtime error:", err.message);
915
+ });
916
+ }
917
+ /** Ping all connected clients every 15s — terminate those that don't respond */
918
+ startHeartbeat(wss) {
919
+ this.heartbeatTimer = setInterval(() => {
920
+ for (const ws of wss.clients) {
921
+ const ext = ws;
922
+ if (ext._rsAlive === false) {
923
+ ws.terminate();
924
+ continue;
925
+ }
926
+ ext._rsAlive = false;
927
+ ws.ping();
928
+ }
929
+ }, 15e3);
930
+ }
847
931
  setupConnectionHandler(wss) {
848
932
  wss.on("connection", (ws) => {
933
+ const ext = ws;
934
+ ext._rsAlive = true;
935
+ ws.on("pong", () => {
936
+ ext._rsAlive = true;
937
+ });
849
938
  if (this.authManager?.isEnabled()) {
850
939
  this.pendingHandshakes.add(ws);
851
940
  const authTimeout = setTimeout(() => {
@@ -1040,10 +1129,27 @@ var CollectorServer = class {
1040
1129
  });
1041
1130
  }
1042
1131
  stop() {
1132
+ if (this.heartbeatTimer) {
1133
+ clearInterval(this.heartbeatTimer);
1134
+ this.heartbeatTimer = null;
1135
+ }
1043
1136
  if (this.pruneTimer) {
1044
1137
  clearInterval(this.pruneTimer);
1045
1138
  this.pruneTimer = null;
1046
1139
  }
1140
+ if (this.wss) {
1141
+ for (const client of this.wss.clients) {
1142
+ if (client.readyState === 1) {
1143
+ try {
1144
+ client.send(JSON.stringify({
1145
+ type: "__server_restart",
1146
+ timestamp: Date.now()
1147
+ }));
1148
+ } catch {
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1047
1153
  for (const [name, sqliteStore] of this.sqliteStores) {
1048
1154
  try {
1049
1155
  sqliteStore.close();
@@ -1061,7 +1167,7 @@ var CollectorServer = class {
1061
1167
  };
1062
1168
 
1063
1169
  // src/project-manager.ts
1064
- import { mkdirSync, readFileSync as readFileSync2, writeFileSync, existsSync, readdirSync } from "fs";
1170
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, readdirSync } from "fs";
1065
1171
  import { join } from "path";
1066
1172
  import { homedir } from "os";
1067
1173
  var DEFAULT_GLOBAL_CONFIG = {
@@ -1093,7 +1199,7 @@ var ProjectManager = class {
1093
1199
  this.mkdirp(this.baseDir);
1094
1200
  this.mkdirp(join(this.baseDir, "projects"));
1095
1201
  const configPath = join(this.baseDir, "config.json");
1096
- if (!existsSync(configPath)) {
1202
+ if (!existsSync2(configPath)) {
1097
1203
  this.writeJson(configPath, DEFAULT_GLOBAL_CONFIG);
1098
1204
  }
1099
1205
  }
@@ -1101,7 +1207,7 @@ var ProjectManager = class {
1101
1207
  const projectDir = this.getProjectDir(projectName);
1102
1208
  this.mkdirp(projectDir);
1103
1209
  const configPath = join(projectDir, "config.json");
1104
- if (!existsSync(configPath)) {
1210
+ if (!existsSync2(configPath)) {
1105
1211
  const config = {
1106
1212
  name: projectName,
1107
1213
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1115,7 +1221,7 @@ var ProjectManager = class {
1115
1221
  // --- Config ---
1116
1222
  getGlobalConfig() {
1117
1223
  const configPath = join(this.baseDir, "config.json");
1118
- if (!existsSync(configPath)) return { ...DEFAULT_GLOBAL_CONFIG };
1224
+ if (!existsSync2(configPath)) return { ...DEFAULT_GLOBAL_CONFIG };
1119
1225
  return { ...DEFAULT_GLOBAL_CONFIG, ...this.readJson(configPath) };
1120
1226
  }
1121
1227
  saveGlobalConfig(config) {
@@ -1123,7 +1229,7 @@ var ProjectManager = class {
1123
1229
  }
1124
1230
  getProjectConfig(projectName) {
1125
1231
  const configPath = join(this.getProjectDir(projectName), "config.json");
1126
- if (!existsSync(configPath)) return null;
1232
+ if (!existsSync2(configPath)) return null;
1127
1233
  return this.readJson(configPath);
1128
1234
  }
1129
1235
  saveProjectConfig(projectName, config) {
@@ -1131,12 +1237,12 @@ var ProjectManager = class {
1131
1237
  }
1132
1238
  getInfrastructureConfig(projectName) {
1133
1239
  const jsonPath = join(this.getProjectDir(projectName), "infrastructure.json");
1134
- if (existsSync(jsonPath)) {
1240
+ if (existsSync2(jsonPath)) {
1135
1241
  const config = this.readJson(jsonPath);
1136
1242
  return this.resolveConfigEnvVars(config);
1137
1243
  }
1138
1244
  const yamlPath = join(this.getProjectDir(projectName), "infrastructure.yaml");
1139
- if (existsSync(yamlPath)) {
1245
+ if (existsSync2(yamlPath)) {
1140
1246
  try {
1141
1247
  const content = readFileSync2(yamlPath, "utf-8");
1142
1248
  return this.resolveConfigEnvVars(this.parseSimpleYaml(content));
@@ -1148,17 +1254,17 @@ var ProjectManager = class {
1148
1254
  }
1149
1255
  getClaudeInstructions(projectName) {
1150
1256
  const filePath = join(this.getProjectDir(projectName), "claude-instructions.md");
1151
- if (!existsSync(filePath)) return null;
1257
+ if (!existsSync2(filePath)) return null;
1152
1258
  return readFileSync2(filePath, "utf-8");
1153
1259
  }
1154
1260
  // --- Discovery ---
1155
1261
  listProjects() {
1156
1262
  const projectsDir = join(this.baseDir, "projects");
1157
- if (!existsSync(projectsDir)) return [];
1263
+ if (!existsSync2(projectsDir)) return [];
1158
1264
  return readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1159
1265
  }
1160
1266
  projectExists(projectName) {
1161
- return existsSync(this.getProjectDir(projectName));
1267
+ return existsSync2(this.getProjectDir(projectName));
1162
1268
  }
1163
1269
  // --- Environment variable resolution ---
1164
1270
  resolveEnvVars(value) {
@@ -1168,7 +1274,7 @@ var ProjectManager = class {
1168
1274
  }
1169
1275
  // --- Private helpers ---
1170
1276
  mkdirp(dir) {
1171
- if (!existsSync(dir)) {
1277
+ if (!existsSync2(dir)) {
1172
1278
  mkdirSync(dir, { recursive: true });
1173
1279
  }
1174
1280
  }
@@ -1508,14 +1614,14 @@ var SessionManager = class {
1508
1614
  // src/http-server.ts
1509
1615
  import { createServer } from "http";
1510
1616
  import { createServer as createHttpsServer2 } from "https";
1511
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
1617
+ import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
1512
1618
  import { resolve, dirname } from "path";
1513
1619
  import { fileURLToPath } from "url";
1514
1620
  import { WebSocketServer as WebSocketServer2 } from "ws";
1515
1621
 
1516
1622
  // src/pm/pm-routes.ts
1517
1623
  import { readdir, readFile, writeFile, unlink, mkdir } from "fs/promises";
1518
- import { existsSync as existsSync2 } from "fs";
1624
+ import { existsSync as existsSync3 } from "fs";
1519
1625
  import { join as join2 } from "path";
1520
1626
  import { homedir as homedir2 } from "os";
1521
1627
  import { spawn, execSync, execFileSync } from "child_process";
@@ -2450,7 +2556,7 @@ function parseGitStatus(porcelain) {
2450
2556
  }
2451
2557
  async function readRuleFile(filePath) {
2452
2558
  try {
2453
- if (existsSync2(filePath)) {
2559
+ if (existsSync3(filePath)) {
2454
2560
  const content = await readFile(filePath, "utf-8");
2455
2561
  return { path: filePath, content, exists: true };
2456
2562
  }
@@ -2745,7 +2851,7 @@ var HttpServer = class {
2745
2851
  // npm installed
2746
2852
  ];
2747
2853
  for (const p of candidates) {
2748
- if (existsSync3(p)) {
2854
+ if (existsSync4(p)) {
2749
2855
  this.sdkBundlePath = p;
2750
2856
  return p;
2751
2857
  }
@@ -4101,7 +4207,7 @@ async function parseSessionJsonl(jsonlPath, sessionId, projectId) {
4101
4207
  // src/pm/project-discovery.ts
4102
4208
  import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
4103
4209
  import { join as join3, basename as basename2 } from "path";
4104
- import { existsSync as existsSync4 } from "fs";
4210
+ import { existsSync as existsSync5 } from "fs";
4105
4211
  import { homedir as homedir3 } from "os";
4106
4212
  var LOG_PREFIX = "[RuntimeScope PM]";
4107
4213
  async function detectSdkInstalled(projectPath) {
@@ -4168,7 +4274,7 @@ function slugifyPath(fsPath) {
4168
4274
  }
4169
4275
  function decodeClaudeKey(key) {
4170
4276
  const naive = "/" + key.slice(1).replace(/-/g, "/");
4171
- if (existsSync4(naive)) return naive;
4277
+ if (existsSync5(naive)) return naive;
4172
4278
  const parts = key.slice(1).split("-");
4173
4279
  return resolvePathSegments(parts);
4174
4280
  }
@@ -4176,16 +4282,16 @@ function resolvePathSegments(parts) {
4176
4282
  if (parts.length === 0) return null;
4177
4283
  function tryResolve(prefix, remaining) {
4178
4284
  if (remaining.length === 0) {
4179
- return existsSync4(prefix) ? prefix : null;
4285
+ return existsSync5(prefix) ? prefix : null;
4180
4286
  }
4181
4287
  for (let count = remaining.length; count >= 1; count--) {
4182
4288
  const segment = remaining.slice(0, count).join("-");
4183
4289
  const candidate = join3(prefix, segment);
4184
4290
  if (count === remaining.length) {
4185
- if (existsSync4(candidate)) return candidate;
4291
+ if (existsSync5(candidate)) return candidate;
4186
4292
  } else {
4187
4293
  try {
4188
- if (existsSync4(candidate)) {
4294
+ if (existsSync5(candidate)) {
4189
4295
  const result = tryResolve(candidate, remaining.slice(count));
4190
4296
  if (result) return result;
4191
4297
  }
@@ -4679,4 +4785,4 @@ export {
4679
4785
  parseSessionJsonl,
4680
4786
  ProjectDiscovery
4681
4787
  };
4682
- //# sourceMappingURL=chunk-VZSMLTUQ.js.map
4788
+ //# sourceMappingURL=chunk-TUFSIGGJ.js.map