@youtyan/code-viewer 0.2.0 → 0.2.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.
@@ -589,12 +589,28 @@ function startServer(options) {
589
589
  resolve({
590
590
  port,
591
591
  close: () => new Promise((resolveClose, rejectClose) => {
592
- server.close((error) => {
593
- if (error)
592
+ let settled = false;
593
+ let forceTimer = null;
594
+ const settle = (error) => {
595
+ if (settled)
596
+ return;
597
+ settled = true;
598
+ if (forceTimer)
599
+ clearTimeout(forceTimer);
600
+ const code = error && "code" in error ? String(error.code) : "";
601
+ if (error && code !== "ERR_SERVER_NOT_RUNNING") {
594
602
  rejectClose(error);
595
- else
596
- resolveClose();
597
- });
603
+ return;
604
+ }
605
+ resolveClose();
606
+ };
607
+ forceTimer = setTimeout(() => {
608
+ server.closeAllConnections?.();
609
+ settle();
610
+ }, 2000);
611
+ forceTimer.unref?.();
612
+ server.close(settle);
613
+ server.closeIdleConnections?.();
598
614
  server.closeAllConnections?.();
599
615
  })
600
616
  });
@@ -3500,31 +3516,85 @@ var init_sql_snapshot = __esm(() => {
3500
3516
 
3501
3517
  // web-src/server/database/adapters/docker-utils.ts
3502
3518
  import { spawnSync as spawnSync2 } from "node:child_process";
3519
+ function isDockerComposeServiceUnavailableError(err) {
3520
+ return err instanceof DockerComposeServiceUnavailableError;
3521
+ }
3522
+ function composeContainerNameCacheKey(serviceName, cwd) {
3523
+ return `${cwd}\x00${serviceName}`;
3524
+ }
3503
3525
  function resolveRunningComposeContainerName(serviceName, cwd) {
3504
- const proc = spawnSync2("docker", ["compose", "ps", "--format", "json", "--status", "running"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "pipe"], cwd });
3505
- if (proc.status !== 0)
3526
+ const cacheKey = composeContainerNameCacheKey(serviceName, cwd);
3527
+ const now = Date.now();
3528
+ const cached = composeContainerNameCache.get(cacheKey);
3529
+ if (cached && cached.expiresAt > now)
3530
+ return cached.value;
3531
+ const proc = spawnSyncImpl("docker", ["compose", "ps", "--format", "json", "--status", "running"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "pipe"], cwd });
3532
+ if (proc.status !== 0) {
3533
+ composeContainerNameCache.set(cacheKey, {
3534
+ value: null,
3535
+ expiresAt: now + COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS
3536
+ });
3506
3537
  return null;
3538
+ }
3507
3539
  try {
3508
3540
  const output = proc.stdout.trim();
3509
3541
  const containers = output.startsWith("[") ? JSON.parse(output) : output.split(`
3510
3542
  `).filter(Boolean).map((line) => JSON.parse(line));
3511
3543
  const match = containers.find((c) => c.Service === serviceName && c.State === "running");
3512
- return match?.Name || null;
3544
+ const value = match?.Name || null;
3545
+ composeContainerNameCache.set(cacheKey, {
3546
+ value,
3547
+ expiresAt: now + (value ? COMPOSE_CONTAINER_NAME_POSITIVE_TTL_MS : COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS)
3548
+ });
3549
+ return value;
3513
3550
  } catch {
3551
+ composeContainerNameCache.set(cacheKey, {
3552
+ value: null,
3553
+ expiresAt: now + COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS
3554
+ });
3514
3555
  return null;
3515
3556
  }
3516
3557
  }
3517
3558
  function resolveRunningComposeContainerNameOrThrow(serviceName, cwd) {
3518
3559
  const containerName = resolveRunningComposeContainerName(serviceName, cwd);
3519
3560
  if (!containerName) {
3520
- throw new Error(`Container for service "${serviceName}" is not running. Start it with: docker compose up -d ${serviceName}`);
3561
+ throw new DockerComposeServiceUnavailableError(serviceName, cwd);
3521
3562
  }
3522
3563
  return containerName;
3523
3564
  }
3524
- var init_docker_utils = () => {};
3565
+ var COMPOSE_CONTAINER_NAME_POSITIVE_TTL_MS = 30000, COMPOSE_CONTAINER_NAME_NEGATIVE_TTL_MS = 3000, composeContainerNameCache, spawnSyncImpl, DockerComposeServiceUnavailableError;
3566
+ var init_docker_utils = __esm(() => {
3567
+ composeContainerNameCache = new Map;
3568
+ spawnSyncImpl = spawnSync2;
3569
+ DockerComposeServiceUnavailableError = class DockerComposeServiceUnavailableError extends Error {
3570
+ serviceName;
3571
+ cwd;
3572
+ status = 503;
3573
+ constructor(serviceName, cwd) {
3574
+ super(`Container for service "${serviceName}" is not running. Start it with: docker compose up -d ${serviceName}`);
3575
+ this.name = "DockerComposeServiceUnavailableError";
3576
+ this.serviceName = serviceName;
3577
+ this.cwd = cwd;
3578
+ }
3579
+ };
3580
+ });
3525
3581
 
3526
3582
  // web-src/server/database/adapters/docker.ts
3527
3583
  import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
3584
+ function dockerDatabasesCacheKey(serviceName, kind, cwd) {
3585
+ return `${serviceName}\x00${kind}\x00${cwd}`;
3586
+ }
3587
+ function setDockerDatabasesCache(key, value, ttlMs, now = Date.now()) {
3588
+ const cachedValue = [...value];
3589
+ dockerDatabasesCache.set(key, {
3590
+ value: cachedValue,
3591
+ expiresAt: now + ttlMs
3592
+ });
3593
+ return [...cachedValue];
3594
+ }
3595
+ function fallbackDockerDatabases(defaultDb) {
3596
+ return defaultDb ? [defaultDb] : [];
3597
+ }
3528
3598
  function buildExecArgs(config, sql) {
3529
3599
  if (config.kind === "postgresql") {
3530
3600
  return [
@@ -3563,7 +3633,6 @@ function buildExecArgs(config, sql) {
3563
3633
  config.user,
3564
3634
  config.database,
3565
3635
  "--batch",
3566
- "--raw",
3567
3636
  "--default-character-set=utf8mb4",
3568
3637
  "-e",
3569
3638
  sql
@@ -3571,7 +3640,7 @@ function buildExecArgs(config, sql) {
3571
3640
  }
3572
3641
  function execInContainer(config, sql, timeoutMs = 1e4) {
3573
3642
  const args = buildExecArgs(config, sql);
3574
- const proc = spawnSync3(args[0], args.slice(1), {
3643
+ const proc = spawnSyncImpl2(args[0], args.slice(1), {
3575
3644
  encoding: "utf8",
3576
3645
  timeout: timeoutMs,
3577
3646
  stdio: ["ignore", "pipe", "pipe"]
@@ -3670,17 +3739,71 @@ async function execInContainerAsync(config, sql, timeoutMs = 1e4) {
3670
3739
  return execWithBunSpawn(bunSpawn, args, timeoutMs);
3671
3740
  return execWithNodeSpawn(args, timeoutMs);
3672
3741
  }
3742
+ function stripFinalLineBreak(text) {
3743
+ if (text.endsWith(`\r
3744
+ `))
3745
+ return text.slice(0, -2);
3746
+ if (text.endsWith(`
3747
+ `) || text.endsWith("\r"))
3748
+ return text.slice(0, -1);
3749
+ return text;
3750
+ }
3751
+ function decodeMysqlBatchField(value) {
3752
+ let out = "";
3753
+ for (let i = 0;i < value.length; i++) {
3754
+ const ch = value[i];
3755
+ if (ch !== "\\" || i + 1 >= value.length) {
3756
+ out += ch;
3757
+ continue;
3758
+ }
3759
+ const next = value[++i];
3760
+ switch (next) {
3761
+ case "0":
3762
+ out += "\x00";
3763
+ break;
3764
+ case "b":
3765
+ out += "\b";
3766
+ break;
3767
+ case "n":
3768
+ out += `
3769
+ `;
3770
+ break;
3771
+ case "r":
3772
+ out += "\r";
3773
+ break;
3774
+ case "t":
3775
+ out += "\t";
3776
+ break;
3777
+ case "Z":
3778
+ out += "\x1A";
3779
+ break;
3780
+ case "\\":
3781
+ out += "\\";
3782
+ break;
3783
+ default:
3784
+ out += `\\${next}`;
3785
+ break;
3786
+ }
3787
+ }
3788
+ return out;
3789
+ }
3790
+ function splitTsvLine(line, decodeFields) {
3791
+ const fields = line.split("\t");
3792
+ return decodeFields ? fields.map(decodeMysqlBatchField) : fields;
3793
+ }
3673
3794
  function parseTsvOutput(stdout, hasHeader) {
3674
- const lines = stdout.trim().split(`
3675
- `).filter(Boolean);
3795
+ const text = stripFinalLineBreak(stdout);
3796
+ if (text.length === 0)
3797
+ return { columns: [], rows: [] };
3798
+ const lines = text.split(/\r?\n/);
3676
3799
  if (lines.length === 0)
3677
3800
  return { columns: [], rows: [] };
3678
3801
  if (hasHeader) {
3679
- const columns = lines[0].split("\t");
3680
- const rows2 = lines.slice(1).map((line) => line.split("\t"));
3802
+ const columns = splitTsvLine(lines[0], true);
3803
+ const rows2 = lines.slice(1).map((line) => splitTsvLine(line, true));
3681
3804
  return { columns, rows: rows2 };
3682
3805
  }
3683
- const rows = lines.map((line) => line.split("\t"));
3806
+ const rows = lines.map((line) => splitTsvLine(line, false));
3684
3807
  return { columns: [], rows };
3685
3808
  }
3686
3809
  function sanitizeIdentifier(name, kind) {
@@ -3694,6 +3817,23 @@ function buildOrderClause(orderBy, kind) {
3694
3817
  const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
3695
3818
  return ` ORDER BY ${parts.join(", ")}`;
3696
3819
  }
3820
+ function isMysqlSpatialType(type) {
3821
+ const baseType = type.trim().toLowerCase().split(/[\s(]/, 1)[0];
3822
+ return MYSQL_SPATIAL_TYPES.has(baseType);
3823
+ }
3824
+ function buildTableSelectList(columns, kind) {
3825
+ if (kind !== "mysql")
3826
+ return "*";
3827
+ let hasSpatialColumn = false;
3828
+ const parts = columns.map((column) => {
3829
+ const columnId = sanitizeIdentifier(column.name, kind);
3830
+ if (!isMysqlSpatialType(column.type))
3831
+ return columnId;
3832
+ hasSpatialColumn = true;
3833
+ return `ST_AsText(${columnId}) AS ${columnId}`;
3834
+ });
3835
+ return hasSpatialColumn ? parts.join(", ") : "*";
3836
+ }
3697
3837
  function escapeSqlString(value) {
3698
3838
  return `'${value.replace(/'/g, "''")}'`;
3699
3839
  }
@@ -4018,12 +4158,15 @@ function createDockerAdapter(config) {
4018
4158
  async getTablePageWithMeta(table, options) {
4019
4159
  const id = sanitizeIdentifier(table, config.kind);
4020
4160
  const order = buildOrderClause(options.orderBy, config.kind);
4021
- const dataSql = `SELECT * FROM ${id}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
4022
4161
  const countSql = `SELECT COUNT(*) AS cnt FROM ${id}`;
4023
- const [columns, dataResult, totalRows] = await Promise.all([
4024
- tableMetaCache.getColumns(table, () => fetchColumnsAsyncUncached(table)),
4162
+ const columnsPromise = tableMetaCache.getColumns(table, () => fetchColumnsAsyncUncached(table));
4163
+ const totalRowsPromise = tableMetaCache.getRowCount(table, async () => rowCountFromResult(await execAsync(countSql)));
4164
+ const columns = await columnsPromise;
4165
+ const selectList = buildTableSelectList(columns, config.kind);
4166
+ const dataSql = `SELECT ${selectList} FROM ${id}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
4167
+ const [dataResult, totalRows] = await Promise.all([
4025
4168
  execAsync(dataSql),
4026
- tableMetaCache.getRowCount(table, async () => rowCountFromResult(await execAsync(countSql)))
4169
+ totalRowsPromise
4027
4170
  ]);
4028
4171
  return tablePageMetaFromResults(columns, dataResult, totalRows);
4029
4172
  },
@@ -4032,21 +4175,25 @@ function createDockerAdapter(config) {
4032
4175
  const order = buildOrderClause(options.orderBy, config.kind);
4033
4176
  const where = buildDockerFilterWhere(options.grouped, config.kind);
4034
4177
  const whereClause = where ? ` WHERE ${where}` : "";
4035
- const dataSql = `SELECT * FROM ${id}${whereClause}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
4036
4178
  const countSql = `SELECT COUNT(*) AS cnt FROM ${id}${whereClause}`;
4037
- const [columns, dataResult, countResult] = await Promise.all([
4038
- tableMetaCache.getColumns(table, () => fetchColumnsAsyncUncached(table)),
4179
+ const columnsPromise = tableMetaCache.getColumns(table, () => fetchColumnsAsyncUncached(table));
4180
+ const countResultPromise = execAsync(countSql);
4181
+ const columns = await columnsPromise;
4182
+ const selectList = buildTableSelectList(columns, config.kind);
4183
+ const dataSql = `SELECT ${selectList} FROM ${id}${whereClause}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
4184
+ const [dataResult, countResult] = await Promise.all([
4039
4185
  execAsync(dataSql),
4040
- execAsync(countSql)
4186
+ countResultPromise
4041
4187
  ]);
4042
4188
  return tablePageMetaFromResults(columns, dataResult, rowCountFromResult(countResult));
4043
4189
  },
4044
4190
  getTablePage(table, options) {
4045
4191
  const id = sanitizeIdentifier(table, config.kind);
4046
4192
  const order = buildOrderClause(options.orderBy, config.kind);
4047
- const sql = `SELECT * FROM ${id}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
4048
- const result = exec(sql);
4049
4193
  const cols = this.getColumns(table);
4194
+ const selectList = buildTableSelectList(cols, config.kind);
4195
+ const sql = `SELECT ${selectList} FROM ${id}${order} LIMIT ${options.limit} OFFSET ${options.offset}`;
4196
+ const result = exec(sql);
4050
4197
  if (result.rows.length === 0) {
4051
4198
  return {
4052
4199
  columns: cols.map((c) => c.name),
@@ -4163,34 +4310,48 @@ function createDockerAdapter(config) {
4163
4310
  return adapter;
4164
4311
  }
4165
4312
  function listDockerDatabases(serviceName, kind, env, cwd) {
4313
+ const cacheKey = dockerDatabasesCacheKey(serviceName, kind, cwd);
4314
+ const now = Date.now();
4315
+ const cached = dockerDatabasesCache.get(cacheKey);
4316
+ if (cached && cached.expiresAt > now)
4317
+ return [...cached.value];
4166
4318
  const containerName = resolveRunningComposeContainerName(serviceName, cwd);
4167
- if (!containerName)
4168
- return [];
4169
- const user = env.POSTGRES_USER || env.MYSQL_USER || env.MARIADB_USER || (kind === "postgresql" ? "postgres" : "root");
4170
- const password = env.POSTGRES_PASSWORD || env.MYSQL_PASSWORD || env.MYSQL_ROOT_PASSWORD || env.MARIADB_PASSWORD || env.MARIADB_ROOT_PASSWORD || "";
4171
- const defaultDb = env.POSTGRES_DB || env.MYSQL_DATABASE || env.MARIADB_DATABASE || (kind === "postgresql" ? "postgres" : "");
4319
+ if (!containerName) {
4320
+ return setDockerDatabasesCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
4321
+ }
4322
+ const user = env.POSTGRES_USER || env.MYSQL_USER || env.MARIADB_USER || env.POSTGRES_USERNAME || env.MYSQL_USERNAME || env.USER || "root";
4323
+ const password = env.POSTGRES_PASSWORD || env.MYSQL_PASSWORD || env.MARIADB_PASSWORD || "";
4324
+ const defaultDb = env.POSTGRES_DB || env.MYSQL_DATABASE || env.MARIADB_DATABASE || env.DATABASE_NAME || "";
4172
4325
  const config = {
4173
4326
  kind,
4174
4327
  containerName,
4175
4328
  user,
4176
4329
  password,
4177
- database: defaultDb || (kind === "postgresql" ? "postgres" : "mysql")
4330
+ database: defaultDb || (kind === "postgresql" ? user || "postgres" : "mysql")
4178
4331
  };
4179
4332
  try {
4180
4333
  let sql;
4181
4334
  if (kind === "postgresql") {
4182
- sql = `SELECT datname FROM pg_database WHERE datistemplate = false AND datallowconn = true ORDER BY datname`;
4335
+ sql = `SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname`;
4183
4336
  } else {
4184
4337
  sql = `SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema','performance_schema','mysql','sys') ORDER BY schema_name`;
4185
4338
  }
4186
4339
  const result = execInContainer(config, sql);
4187
- if (result.code !== 0)
4188
- return defaultDb ? [defaultDb] : [];
4340
+ if (result.code !== 0) {
4341
+ const fallback = fallbackDockerDatabases(defaultDb);
4342
+ if (fallback.length > 0)
4343
+ return fallback;
4344
+ return setDockerDatabasesCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
4345
+ }
4189
4346
  const parsed = parseTsvOutput(result.stdout, kind === "mysql");
4190
4347
  const dbs = parsed.rows.map((r) => r[0]).filter(Boolean);
4191
- return dbs.length > 0 ? dbs : defaultDb ? [defaultDb] : [];
4348
+ const value = dbs.length > 0 ? dbs : fallbackDockerDatabases(defaultDb);
4349
+ return setDockerDatabasesCache(cacheKey, value, value.length > 0 ? DOCKER_DATABASES_POSITIVE_TTL_MS : DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
4192
4350
  } catch {
4193
- return defaultDb ? [defaultDb] : [];
4351
+ const fallback = fallbackDockerDatabases(defaultDb);
4352
+ if (fallback.length > 0)
4353
+ return fallback;
4354
+ return setDockerDatabasesCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
4194
4355
  }
4195
4356
  }
4196
4357
  function openDockerAdapter(serviceName, kind, env, cwd, overrideDatabase) {
@@ -4206,10 +4367,23 @@ function openDockerAdapter(serviceName, kind, env, cwd, overrideDatabase) {
4206
4367
  database
4207
4368
  });
4208
4369
  }
4209
- var COLUMNS_TTL_MS = 30000, ROWCOUNT_TTL_MS = 15000;
4370
+ var COLUMNS_TTL_MS = 30000, ROWCOUNT_TTL_MS = 15000, DOCKER_DATABASES_POSITIVE_TTL_MS = 15000, DOCKER_DATABASES_NEGATIVE_TTL_MS = 3000, dockerDatabasesCache, spawnSyncImpl2, MYSQL_SPATIAL_TYPES;
4210
4371
  var init_docker = __esm(() => {
4211
4372
  init_sql_snapshot();
4212
4373
  init_docker_utils();
4374
+ dockerDatabasesCache = new Map;
4375
+ spawnSyncImpl2 = spawnSync3;
4376
+ MYSQL_SPATIAL_TYPES = new Set([
4377
+ "geometry",
4378
+ "point",
4379
+ "linestring",
4380
+ "polygon",
4381
+ "multipoint",
4382
+ "multilinestring",
4383
+ "multipolygon",
4384
+ "geometrycollection",
4385
+ "geomcollection"
4386
+ ]);
4213
4387
  });
4214
4388
 
4215
4389
  // web-src/server/database/adapters/sqlite.ts
@@ -4561,6 +4735,7 @@ function isSqliteFile(fullPath) {
4561
4735
  function discoverSqliteFiles(cwd, omitDirNames) {
4562
4736
  const omitSet = new Set(omitDirNames.map((d) => d.toLowerCase()));
4563
4737
  omitSet.add(".git");
4738
+ omitSet.add(".code-viewer");
4564
4739
  const results = [];
4565
4740
  function scan(dir, depth) {
4566
4741
  if (depth > MAX_SCAN_DEPTH || results.length >= MAX_ENTRIES)
@@ -4605,20 +4780,14 @@ function discoverSqliteFiles(cwd, omitDirNames) {
4605
4780
  }
4606
4781
  }
4607
4782
  scan(cwd, 0);
4608
- results.sort((a, b) => {
4609
- const aInternal = a.path.startsWith(".code-viewer/") ? 1 : 0;
4610
- const bInternal = b.path.startsWith(".code-viewer/") ? 1 : 0;
4611
- if (aInternal !== bInternal)
4612
- return aInternal - bInternal;
4613
- return a.path.localeCompare(b.path);
4614
- });
4783
+ results.sort((a, b) => a.path.localeCompare(b.path));
4615
4784
  return results;
4616
4785
  }
4617
4786
  function validateDbPath(cwd, dbPath) {
4618
4787
  if (!dbPath || dbPath.includes("\x00") || dbPath.startsWith("/") || dbPath.startsWith("\\"))
4619
4788
  return null;
4620
4789
  const parts = dbPath.split(/[\\/]+/);
4621
- if (parts.some((p) => p === ".." || p.toLowerCase() === ".git"))
4790
+ if (parts.some((p) => p === ".." || p.toLowerCase() === ".git" || p.toLowerCase() === ".code-viewer"))
4622
4791
  return null;
4623
4792
  const full = join8(cwd, dbPath);
4624
4793
  if (!existsSync6(full))
@@ -5045,7 +5214,7 @@ function isTextLikeType(type) {
5045
5214
  return upper.includes("CHAR") || upper.includes("TEXT") || upper.includes("VARCHAR") || upper.includes("CLOB") || upper.includes("STRING") || upper === "JSON" || upper === "JSONB" || upper === "XML" || upper === "UUID";
5046
5215
  }
5047
5216
  function escapeLikeTerm(term) {
5048
- return term.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
5217
+ return term.replace(/=/g, "==").replace(/%/g, "=%").replace(/_/g, "=_");
5049
5218
  }
5050
5219
  function searchTable(adapter, table, columns, term, maxHits, includeNonText, pkColumns) {
5051
5220
  const kind = adapter.kind;
@@ -5063,10 +5232,10 @@ function searchTable(adapter, table, columns, term, maxHits, includeNonText, pkC
5063
5232
  let sql;
5064
5233
  const remaining = maxHits - hits.length;
5065
5234
  if (kind === "sqlite") {
5066
- sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ? ESCAPE '\\' LIMIT ${remaining}`;
5235
+ sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ? ESCAPE '='`;
5067
5236
  } else {
5068
5237
  const likeVal = escapeSqlString2(`%${escapedTerm}%`);
5069
- sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ${likeVal} ESCAPE '\\' LIMIT ${remaining}`;
5238
+ sql = `SELECT * FROM ${tbl} WHERE ${castCol} LIKE ${likeVal} ESCAPE '='`;
5070
5239
  }
5071
5240
  try {
5072
5241
  const result = kind === "sqlite" ? adapter.executeReadonlyQuery(sql, [`%${escapedTerm}%`], remaining) : adapter.executeReadonlyQuery(sql, undefined, remaining);
@@ -5534,7 +5703,7 @@ function resolveDockerExplorer(cwd, dbParam, kind, cache, openFn, omitDirNames)
5534
5703
  const explorer = cache.getOrOpen(dbParam, () => openFn(info));
5535
5704
  return { dbId: dbParam, explorer };
5536
5705
  }
5537
- async function dispatchRoutes(req, url, routes, sideEffectAllowed, wrap = (res) => res) {
5706
+ async function dispatchRoutes(req, url, routes, sideEffectAllowed, wrap = (res) => res, handleRouteError) {
5538
5707
  if (!Object.prototype.hasOwnProperty.call(routes, url.pathname))
5539
5708
  return null;
5540
5709
  const route = routes[url.pathname];
@@ -5545,7 +5714,13 @@ async function dispatchRoutes(req, url, routes, sideEffectAllowed, wrap = (res)
5545
5714
  if (requiresSideEffect && sideEffectAllowed && !sideEffectAllowed(req)) {
5546
5715
  return wrap(textError("forbidden", 403));
5547
5716
  }
5548
- return wrap(await route.handler());
5717
+ try {
5718
+ return wrap(await route.handler());
5719
+ } catch (err) {
5720
+ if (!handleRouteError)
5721
+ throw err;
5722
+ return wrap(handleRouteError(err));
5723
+ }
5549
5724
  }
5550
5725
  async function parsePostJsonBody(req) {
5551
5726
  if (req.method !== "POST") {
@@ -5560,10 +5735,14 @@ async function parsePostJsonBody(req) {
5560
5735
  function handleError(prefix, action, err) {
5561
5736
  const message = err instanceof Error ? err.message : String(err);
5562
5737
  console.error(`[code-viewer] ${prefix} error:`, message);
5738
+ if (isDockerComposeServiceUnavailableError(err)) {
5739
+ return textError(message, err.status);
5740
+ }
5563
5741
  return textError(`failed to ${action}: ${message}`, 500);
5564
5742
  }
5565
5743
  var DEFAULT_MAX_DOCKER_ADAPTER_CACHE = 8, DEFAULT_DOCKER_ADAPTER_IDLE_MS;
5566
5744
  var init_handle_shared = __esm(() => {
5745
+ init_docker_utils();
5567
5746
  init_discovery();
5568
5747
  DEFAULT_DOCKER_ADAPTER_IDLE_MS = 5 * 60 * 1000;
5569
5748
  });
@@ -5741,7 +5920,7 @@ async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDi
5741
5920
  sideEffect: (m) => m === "POST",
5742
5921
  handler: () => handleSearch(cwd, req, url, omitDirNames)
5743
5922
  }
5744
- }, sideEffectAllowed, wrap);
5923
+ }, sideEffectAllowed, wrap, (err) => handleError("elasticsearch", "handle elasticsearch request", err));
5745
5924
  }
5746
5925
  var esAdapterCache;
5747
5926
  var init_handle_elasticsearch = __esm(() => {
@@ -6472,7 +6651,7 @@ async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames)
6472
6651
  methods: ["GET"],
6473
6652
  handler: () => handleValue(cwd, url, omitDirNames)
6474
6653
  }
6475
- }, sideEffectAllowed, wrap);
6654
+ }, sideEffectAllowed, wrap, (err) => handleError("redis", "handle redis request", err));
6476
6655
  }
6477
6656
  function handleValue(cwd, url, omitDirNames) {
6478
6657
  const r = resolveRedis(cwd, url.searchParams.get("db"), omitDirNames);
@@ -6968,6 +7147,11 @@ function sanitizeCssSize(v) {
6968
7147
  return;
6969
7148
  return isValidCssSize(v) ? v : undefined;
6970
7149
  }
7150
+ function isToolInternalDbId(dbId) {
7151
+ if (!dbId || dbId.startsWith("docker:"))
7152
+ return false;
7153
+ return dbId.split(/[\\/]+/).some((part) => part.toLowerCase() === ".code-viewer");
7154
+ }
6971
7155
  function sanitizeRedis(v) {
6972
7156
  if (!v || typeof v !== "object")
6973
7157
  return;
@@ -7023,6 +7207,8 @@ function sanitize(input) {
7023
7207
  continue;
7024
7208
  seenIds.add(id);
7025
7209
  const dbId = sanitizeOptionalString(tab.dbId, MAX_DB_ID_LEN) ?? null;
7210
+ if (isToolInternalDbId(dbId))
7211
+ continue;
7026
7212
  const table = sanitizeOptionalString(tab.table, MAX_TABLE_NAME_LEN) ?? null;
7027
7213
  const view = typeof tab.view === "string" && VALID_VIEWS.has(tab.view) ? tab.view : "data";
7028
7214
  const out = { id, dbId, table, view };
@@ -7497,6 +7683,9 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
7497
7683
  }
7498
7684
  return json(response);
7499
7685
  } catch (err) {
7686
+ if (isDockerComposeServiceUnavailableError(err)) {
7687
+ return handleError("database", "execute query", err);
7688
+ }
7500
7689
  console.error("[code-viewer] database error:", err instanceof Error ? err.message : String(err));
7501
7690
  const elapsed = Date.now() - start;
7502
7691
  const response = {
@@ -7723,7 +7912,7 @@ async function handleSearchStart(cwd, req, omitDirNames) {
7723
7912
  const maxHitsPerTable = body.maxHitsPerTable ?? 50;
7724
7913
  const includeNonText = body.includeNonText ?? false;
7725
7914
  const filterTables = body.tables;
7726
- (async () => {
7915
+ const runJob = async () => {
7727
7916
  try {
7728
7917
  const adapter = await getAdapter(r, cwd);
7729
7918
  let tables = adapter.getTables().filter((t) => t.type === "table").map((t) => t.name);
@@ -7757,7 +7946,9 @@ async function handleSearchStart(cwd, req, omitDirNames) {
7757
7946
  job.error = err instanceof Error ? err.message : String(err);
7758
7947
  job.done = true;
7759
7948
  }
7760
- })();
7949
+ };
7950
+ const timer = setTimeout(() => void runJob(), 0);
7951
+ timer.unref?.();
7761
7952
  return json({ jobId });
7762
7953
  }
7763
7954
  function handleSearchStatus(url) {
@@ -8171,11 +8362,12 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
8171
8362
  sideEffect: (m) => m !== "GET",
8172
8363
  handler: () => method === "GET" ? handleTabsGet(cwd) : handleTabsPut(cwd, req)
8173
8364
  }
8174
- }, sideEffectAllowed, wrapResponse);
8365
+ }, sideEffectAllowed, wrapResponse, (err) => handleError("database", "handle database request", err));
8175
8366
  }
8176
8367
  var initialized = false, dockerAdapterCache, EXPORT_MAX_ROWS = 1e5, MAX_TABS_BODY_BYTES = 1e6, MAX_SNAPSHOT_TABLES = 512, MAX_SNAPSHOT_TABLE_NAME_LEN = 1024, searchJobs, snapshotJobs, DOCKER_CLOSE_REGISTRY, SNAPSHOT_DOCKER_SOURCE_REGISTRY;
8177
8368
  var init_handle = __esm(() => {
8178
8369
  init_docker();
8370
+ init_docker_utils();
8179
8371
  init_sqlite();
8180
8372
  init_connection_pool();
8181
8373
  init_discovery();
@@ -9986,15 +10178,45 @@ data: ${data}
9986
10178
  try {
9987
10179
  client.enqueue(payload);
9988
10180
  } catch {
9989
- sseClients.delete(client);
10181
+ removeSseClient(client);
9990
10182
  }
9991
10183
  }
9992
10184
  }
10185
+ function removeSseClient(ctrl) {
10186
+ sseClients.delete(ctrl);
10187
+ const keepalive = sseKeepalives.get(ctrl);
10188
+ if (keepalive)
10189
+ clearInterval(keepalive);
10190
+ sseKeepalives.delete(ctrl);
10191
+ }
10192
+ function closeSseClients() {
10193
+ for (const client of [...sseClients]) {
10194
+ removeSseClient(client);
10195
+ try {
10196
+ client.close();
10197
+ } catch {}
10198
+ }
10199
+ }
9993
10200
  function openBrowser(url) {
9994
10201
  const cmd = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd.exe", "/c", "start", "", url] : ["xdg-open", url];
9995
10202
  spawnDetached(cmd);
9996
10203
  }
9997
- var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, uploadDisabledByConfig = false, rgAvailableCache = null, enc, sseClients, fileCache, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, server;
10204
+ async function shutdown(exitCode = 0) {
10205
+ if (shuttingDown) {
10206
+ process.exit(1);
10207
+ }
10208
+ shuttingDown = true;
10209
+ removeServerRegistry(cwd, process.pid);
10210
+ closeSseClients();
10211
+ worktreeWatch?.close();
10212
+ try {
10213
+ await server.close();
10214
+ } catch (error) {
10215
+ console.warn(`code-viewer server close skipped: ${String(error)}`);
10216
+ }
10217
+ process.exit(exitCode);
10218
+ }
10219
+ var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, uploadDisabledByConfig = false, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, server, worktreeWatch = null, shuttingDown = false;
9998
10220
  var init_preview = __esm(async () => {
9999
10221
  init_routes();
10000
10222
  init_annotations();
@@ -10057,6 +10279,7 @@ var init_preview = __esm(async () => {
10057
10279
  scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
10058
10280
  enc = new TextEncoder;
10059
10281
  sseClients = new Set;
10282
+ sseKeepalives = new Map;
10060
10283
  fileCache = new Map;
10061
10284
  metaCache = new Map;
10062
10285
  fileListCache = new Map;
@@ -10137,16 +10360,15 @@ data: ok
10137
10360
 
10138
10361
  `));
10139
10362
  } catch {
10140
- sseClients.delete(controller);
10141
- clearInterval(keepalive);
10363
+ removeSseClient(controller);
10142
10364
  }
10143
10365
  }, 15000);
10366
+ keepalive.unref?.();
10367
+ sseKeepalives.set(controller, keepalive);
10144
10368
  },
10145
10369
  cancel() {
10146
10370
  if (ctrl)
10147
- sseClients.delete(ctrl);
10148
- if (keepalive)
10149
- clearInterval(keepalive);
10371
+ removeSseClient(ctrl);
10150
10372
  }
10151
10373
  }), {
10152
10374
  headers: {
@@ -10167,12 +10389,13 @@ data: ok
10167
10389
  root: cwd,
10168
10390
  started_at: new Date().toISOString()
10169
10391
  });
10170
- process.on("exit", () => removeServerRegistry(cwd, process.pid));
10392
+ process.on("exit", () => {
10393
+ removeServerRegistry(cwd, process.pid);
10394
+ closeSseClients();
10395
+ worktreeWatch?.close();
10396
+ });
10171
10397
  for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
10172
- process.on(signal, () => {
10173
- removeServerRegistry(cwd, process.pid);
10174
- process.exit(0);
10175
- });
10398
+ process.on(signal, () => void shutdown(0));
10176
10399
  }
10177
10400
  if (process.env.CODE_VIEWER_DEV === "1") {
10178
10401
  const parentPid = process.ppid;
@@ -10181,8 +10404,7 @@ data: ok
10181
10404
  process.kill(parentPid, 0);
10182
10405
  } catch {
10183
10406
  console.log("dev wrapper exited; shutting down preview server");
10184
- removeServerRegistry(cwd, process.pid);
10185
- process.exit(0);
10407
+ shutdown(0);
10186
10408
  }
10187
10409
  }, 1000).unref();
10188
10410
  }
@@ -10193,7 +10415,7 @@ data: ok
10193
10415
  watch,
10194
10416
  sendReload: () => sendSse("reload")
10195
10417
  });
10196
- startWorktreeUpdateWatch({
10418
+ worktreeWatch = startWorktreeUpdateWatch({
10197
10419
  root: cwd,
10198
10420
  omitDirNames: scopeOmitDirNames,
10199
10421
  excludeNames: scopeExcludeNames,