@youtyan/code-viewer 0.2.3 → 0.2.5

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/web/app.js CHANGED
@@ -633,12 +633,14 @@
633
633
  }
634
634
  case "/database": {
635
635
  const db = params.get("db") || undefined;
636
+ const schema = params.get("schema") || undefined;
636
637
  const table = params.get("table") || undefined;
637
638
  const tabRaw = params.get("tab");
638
639
  const tab = tabRaw === "data" || tabRaw === "query" || tabRaw === "schema" || tabRaw === "er" || tabRaw === "search" || tabRaw === "snapshot" ? tabRaw : undefined;
639
640
  return {
640
641
  screen: "database",
641
642
  ...db ? { db } : {},
643
+ ...schema ? { schema } : {},
642
644
  ...table ? { table } : {},
643
645
  ...tab ? { tab } : {},
644
646
  range
@@ -694,6 +696,8 @@
694
696
  const params = new URLSearchParams;
695
697
  if (route.db)
696
698
  params.set("db", route.db);
699
+ if (route.schema)
700
+ params.set("schema", route.schema);
697
701
  if (route.table)
698
702
  params.set("table", route.table);
699
703
  if (route.tab)
@@ -6696,7 +6700,7 @@
6696
6700
  }
6697
6701
  return resolved.join("/");
6698
6702
  }
6699
- function createMarkdownIt(target, highlighter, signal) {
6703
+ function createMarkdownIt(target, highlighter, signal, resolveAssetUrl) {
6700
6704
  const md = new lib_default({
6701
6705
  html: false,
6702
6706
  linkify: true,
@@ -6761,8 +6765,11 @@
6761
6765
  const token = tokens[idx];
6762
6766
  const src = token.attrGet("src") || "";
6763
6767
  const resolved = resolveMarkdownAssetPath(target.path, src);
6764
- if (resolved)
6765
- token.attrSet("src", buildRawFileUrl({ path: resolved, ref: target.ref || "worktree" }));
6768
+ if (resolved) {
6769
+ const assetUrl = resolveAssetUrl ? resolveAssetUrl(resolved, src) : buildRawFileUrl({ path: resolved, ref: target.ref || "worktree" });
6770
+ if (assetUrl)
6771
+ token.attrSet("src", assetUrl);
6772
+ }
6766
6773
  token.attrSet("loading", "lazy");
6767
6774
  return image2(tokens, idx, options, env, self);
6768
6775
  };
@@ -6792,7 +6799,7 @@
6792
6799
  markdown.className = "gdp-markdown-preview markdown-body";
6793
6800
  if (options.signal?.aborted)
6794
6801
  return markdown;
6795
- markdown.innerHTML = renderMarkdownHtml(textValue, target, highlighter, options.signal);
6802
+ markdown.innerHTML = renderMarkdownHtml(textValue, target, highlighter, options.signal, options.resolveAssetUrl);
6796
6803
  if (options.signal?.aborted)
6797
6804
  return markdown;
6798
6805
  enhanceTaskLists(markdown);
@@ -6808,8 +6815,8 @@
6808
6815
  wireMarkdownInteractions(markdown, target, options);
6809
6816
  return markdown;
6810
6817
  }
6811
- function renderMarkdownHtml(textValue, target, highlighter, signal) {
6812
- const md = createMarkdownIt(target, highlighter, signal);
6818
+ function renderMarkdownHtml(textValue, target, highlighter, signal, resolveAssetUrl) {
6819
+ const md = createMarkdownIt(target, highlighter, signal, resolveAssetUrl);
6813
6820
  const frontmatter = splitYamlFrontmatter(textValue);
6814
6821
  if (!frontmatter)
6815
6822
  return md.render(textValue);
@@ -7300,9 +7307,11 @@ ${frontmatter.yaml}
7300
7307
  }
7301
7308
  function annotationLocationLabel(entry) {
7302
7309
  if (entry.target?.kind === "database") {
7303
- const parts = ["Database"];
7310
+ const parts = ["Datastores"];
7304
7311
  if (entry.target.db)
7305
7312
  parts.push(entry.target.db);
7313
+ if (entry.target.schema)
7314
+ parts.push(entry.target.schema);
7306
7315
  if (entry.target.table)
7307
7316
  parts.push(entry.target.table);
7308
7317
  if (entry.target.tab)
@@ -7326,6 +7335,8 @@ ${frontmatter.yaml}
7326
7335
  const target = entry.target;
7327
7336
  if (target.db && target.db !== route.db)
7328
7337
  return false;
7338
+ if (target.schema && target.schema !== route.schema)
7339
+ return false;
7329
7340
  if (target.table && target.table !== route.table)
7330
7341
  return false;
7331
7342
  if (target.tab && target.tab !== (route.tab || "data"))
@@ -7404,7 +7415,7 @@ ${frontmatter.yaml}
7404
7415
  return;
7405
7416
  const strip = document.createElement("section");
7406
7417
  strip.className = "gdp-db-annotation-strip";
7407
- strip.setAttribute("aria-label", "Database annotations");
7418
+ strip.setAttribute("aria-label", "Datastore annotations");
7408
7419
  for (const entry of matches) {
7409
7420
  strip.appendChild(buildDatabaseAnnotationBlock(entry));
7410
7421
  }
@@ -7647,7 +7658,9 @@ ${frontmatter.yaml}
7647
7658
  return firstLine.length > 90 ? `${firstLine.slice(0, 90)}…` : firstLine;
7648
7659
  }
7649
7660
  function databaseAnnotationTitle(target) {
7650
- const parts = [target.table || target.db || "Database"];
7661
+ const parts = [target.table || target.schema || target.db || "Datastores"];
7662
+ if (target.schema && target.table)
7663
+ parts.unshift(target.schema);
7651
7664
  if (target.tab === "data" && target.data?.search)
7652
7665
  parts.push(`search: ${target.data.search}`);
7653
7666
  else if (target.tab === "query" && target.query?.sql)
@@ -7659,7 +7672,7 @@ ${frontmatter.yaml}
7659
7672
  return parts.join(" / ");
7660
7673
  }
7661
7674
  function openDatabaseCaptureForm(target) {
7662
- $("#annotation-detail-session").textContent = activeSessionId || "Database annotations";
7675
+ $("#annotation-detail-session").textContent = activeSessionId || "Datastore annotations";
7663
7676
  $("#annotation-detail-step").textContent = "new";
7664
7677
  const location2 = $("#annotation-detail-location");
7665
7678
  location2.textContent = databaseAnnotationTitle(target);
@@ -7700,7 +7713,7 @@ ${frontmatter.yaml}
7700
7713
  body: JSON.stringify({
7701
7714
  action: "add",
7702
7715
  session_id: activeSessionId || undefined,
7703
- session_title: activeSessionId ? undefined : "Database annotations",
7716
+ session_title: activeSessionId ? undefined : "Datastore annotations",
7704
7717
  target,
7705
7718
  title: titleInput.value,
7706
7719
  body: bodyInput.value
@@ -7954,6 +7967,7 @@ ${frontmatter.yaml}
7954
7967
  deps.setRoute({
7955
7968
  screen: "database",
7956
7969
  db: target.db,
7970
+ schema: target.schema,
7957
7971
  table: target.table,
7958
7972
  tab: target.tab,
7959
7973
  range: deps.currentRange()
@@ -8137,6 +8151,35 @@ ${frontmatter.yaml}
8137
8151
  return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
8138
8152
  }
8139
8153
 
8154
+ // web-src/views/database/abort-guard.ts
8155
+ function createAbortGuard() {
8156
+ let active = null;
8157
+ let runId = 0;
8158
+ return {
8159
+ start() {
8160
+ active?.abort();
8161
+ const abort = new AbortController;
8162
+ active = abort;
8163
+ const requestRunId = ++runId;
8164
+ return {
8165
+ signal: abort.signal,
8166
+ isStale() {
8167
+ return abort.signal.aborted || requestRunId !== runId;
8168
+ },
8169
+ finish() {
8170
+ if (active === abort)
8171
+ active = null;
8172
+ }
8173
+ };
8174
+ },
8175
+ dispose() {
8176
+ runId++;
8177
+ active?.abort();
8178
+ active = null;
8179
+ }
8180
+ };
8181
+ }
8182
+
8140
8183
  // web-src/views/media-embed.ts
8141
8184
  var MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov|mp3|wav|ogg|flac|m4a|aac|opus)(\?.*)?$/i;
8142
8185
  var IMAGE_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i;
@@ -8580,35 +8623,6 @@ ${frontmatter.yaml}
8580
8623
  return fallback.charAt(0).toUpperCase() + fallback.slice(1);
8581
8624
  }
8582
8625
 
8583
- // web-src/views/database/abort-guard.ts
8584
- function createAbortGuard() {
8585
- let active = null;
8586
- let runId = 0;
8587
- return {
8588
- start() {
8589
- active?.abort("abort-guard:start");
8590
- const abort = new AbortController;
8591
- active = abort;
8592
- const requestRunId = ++runId;
8593
- return {
8594
- signal: abort.signal,
8595
- isStale() {
8596
- return abort.signal.aborted || requestRunId !== runId;
8597
- },
8598
- finish() {
8599
- if (active === abort)
8600
- active = null;
8601
- }
8602
- };
8603
- },
8604
- dispose() {
8605
- runId++;
8606
- active?.abort("abort-guard:dispose");
8607
- active = null;
8608
- }
8609
- };
8610
- }
8611
-
8612
8626
  // web-src/views/database/pane-status.ts
8613
8627
  function setPaneStatus(el, message, options = {}) {
8614
8628
  el.innerHTML = "";
@@ -8692,6 +8706,10 @@ ${frontmatter.yaml}
8692
8706
  let suppressNotify = false;
8693
8707
  let queryNotifyTimer = null;
8694
8708
  let disposed = false;
8709
+ let activeIndexRow = null;
8710
+ let activeDocRow = null;
8711
+ const indexRowsByName = new Map;
8712
+ const docRowsById = new Map;
8695
8713
  const indexGuard = createAbortGuard();
8696
8714
  const mappingGuard = createAbortGuard();
8697
8715
  const docsGuard = createAbortGuard();
@@ -8717,14 +8735,17 @@ ${frontmatter.yaml}
8717
8735
  }
8718
8736
  function renderIndices(indices) {
8719
8737
  indexList.innerHTML = "";
8738
+ indexRowsByName.clear();
8720
8739
  if (indices.length === 0) {
8721
8740
  setIndexStatus("(no indices)");
8722
8741
  return;
8723
8742
  }
8743
+ const fragment = document.createDocumentFragment();
8724
8744
  for (const ix of indices) {
8725
8745
  const item = document.createElement("div");
8726
8746
  item.className = "es-index-item";
8727
8747
  item.dataset.indexName = ix.name;
8748
+ indexRowsByName.set(ix.name, item);
8728
8749
  const name = document.createElement("span");
8729
8750
  name.className = "es-index-name";
8730
8751
  name.textContent = ix.name;
@@ -8733,20 +8754,24 @@ ${frontmatter.yaml}
8733
8754
  meta.className = "es-index-meta";
8734
8755
  meta.textContent = `${ix.docCount.toLocaleString()} docs / ${formatBytes(ix.sizeBytes)}`;
8735
8756
  item.append(name, meta);
8736
- item.addEventListener("click", () => selectIndex(ix.name));
8737
- indexList.appendChild(item);
8757
+ fragment.appendChild(item);
8738
8758
  }
8759
+ indexList.appendChild(fragment);
8739
8760
  }
8740
8761
  function highlightActiveIndex(name) {
8741
- for (const item of indexList.querySelectorAll(".es-index-item")) {
8742
- item.classList.toggle("active", item.dataset.indexName === name);
8743
- }
8762
+ if (activeIndexRow?.dataset.indexName === name)
8763
+ return;
8764
+ activeIndexRow?.classList.remove("active");
8765
+ activeIndexRow = indexRowsByName.get(name) ?? null;
8766
+ activeIndexRow?.classList.add("active");
8744
8767
  }
8745
8768
  function appendDocs(hits) {
8769
+ const fragment = document.createDocumentFragment();
8746
8770
  for (const hit of hits) {
8747
8771
  const row = document.createElement("div");
8748
8772
  row.className = "es-doc-item";
8749
8773
  row.dataset.docId = hit._id;
8774
+ docRowsById.set(hit._id, row);
8750
8775
  const id = document.createElement("span");
8751
8776
  id.className = "es-doc-id";
8752
8777
  id.textContent = hit._id;
@@ -8755,24 +8780,38 @@ ${frontmatter.yaml}
8755
8780
  preview.className = "es-doc-preview";
8756
8781
  preview.textContent = previewSource(hit._source);
8757
8782
  row.append(id, preview);
8758
- row.addEventListener("click", () => selectDoc(hit._id));
8759
- docList.appendChild(row);
8783
+ fragment.appendChild(row);
8760
8784
  }
8785
+ docList.appendChild(fragment);
8761
8786
  }
8762
8787
  function previewSource(src) {
8763
8788
  if (src === null || src === undefined)
8764
8789
  return "";
8765
- try {
8766
- const s2 = JSON.stringify(src);
8767
- return s2.length > 200 ? `${s2.slice(0, 200)}…` : s2;
8768
- } catch {
8769
- return String(src);
8790
+ if (Array.isArray(src))
8791
+ return `array(${src.length})`;
8792
+ if (typeof src === "object") {
8793
+ const entries = Object.entries(src).slice(0, 6);
8794
+ const parts = entries.map(([key, value]) => {
8795
+ if (value === null)
8796
+ return `${key}=null`;
8797
+ if (Array.isArray(value))
8798
+ return `${key}=array(${value.length})`;
8799
+ if (typeof value === "object")
8800
+ return `${key}=object`;
8801
+ const text3 = String(value);
8802
+ return `${key}=${text3.length > 40 ? `${text3.slice(0, 40)}...` : text3}`;
8803
+ });
8804
+ return parts.join(" / ");
8770
8805
  }
8806
+ const text2 = String(src);
8807
+ return text2.length > 200 ? `${text2.slice(0, 200)}...` : text2;
8771
8808
  }
8772
8809
  function highlightActiveDoc(id) {
8773
- for (const row of docList.querySelectorAll(".es-doc-item")) {
8774
- row.classList.toggle("active", row.dataset.docId === id);
8775
- }
8810
+ if (activeDocRow?.dataset.docId === id)
8811
+ return;
8812
+ activeDocRow?.classList.remove("active");
8813
+ activeDocRow = docRowsById.get(id) ?? null;
8814
+ activeDocRow?.classList.add("active");
8776
8815
  }
8777
8816
  function setDetailTab(tab) {
8778
8817
  detailTab = tab;
@@ -8902,6 +8941,8 @@ ${frontmatter.yaml}
8902
8941
  if (!append) {
8903
8942
  lastSort = undefined;
8904
8943
  setDocStatus("Loading docs...");
8944
+ docRowsById.clear();
8945
+ activeDocRow = null;
8905
8946
  }
8906
8947
  try {
8907
8948
  const params = new URLSearchParams({
@@ -8927,8 +8968,10 @@ ${frontmatter.yaml}
8927
8968
  if (disposed || slot.isStale() || requestRunId !== loadRunId || requestDbId !== currentDbId || requestIndex !== currentIndex || data.index !== requestIndex || requestQuery !== currentQuery) {
8928
8969
  return;
8929
8970
  }
8930
- if (!append)
8971
+ if (!append) {
8931
8972
  docList.innerHTML = "";
8973
+ docRowsById.clear();
8974
+ }
8932
8975
  if (data.hits.length === 0 && !append) {
8933
8976
  setDocStatus("(no docs)");
8934
8977
  } else {
@@ -9028,6 +9071,22 @@ ${frontmatter.yaml}
9028
9071
  }, 300);
9029
9072
  });
9030
9073
  docMoreBtn.addEventListener("click", () => loadDocs(true));
9074
+ indexList.addEventListener("click", (e2) => {
9075
+ const row = e2.target?.closest(".es-index-item");
9076
+ if (!row || !indexList.contains(row))
9077
+ return;
9078
+ const name = row.dataset.indexName;
9079
+ if (name)
9080
+ selectIndex(name);
9081
+ });
9082
+ docList.addEventListener("click", (e2) => {
9083
+ const row = e2.target?.closest(".es-doc-item");
9084
+ if (!row || !docList.contains(row))
9085
+ return;
9086
+ const id = row.dataset.docId;
9087
+ if (id)
9088
+ selectDoc(id);
9089
+ });
9031
9090
  async function load(dbId, initial) {
9032
9091
  if (disposed)
9033
9092
  return;
@@ -9050,6 +9109,9 @@ ${frontmatter.yaml}
9050
9109
  searchInput.value = initial?.query ?? "";
9051
9110
  currentQuery = searchInput.value.trim();
9052
9111
  docList.innerHTML = "";
9112
+ docRowsById.clear();
9113
+ activeIndexRow = null;
9114
+ activeDocRow = null;
9053
9115
  docMoreBtn.hidden = true;
9054
9116
  mappingBody.innerHTML = "";
9055
9117
  mappingBody.textContent = "Select an index to view its mapping.";
@@ -9106,6 +9168,10 @@ ${frontmatter.yaml}
9106
9168
  searchInput.value = "";
9107
9169
  indexList.innerHTML = "";
9108
9170
  docList.innerHTML = "";
9171
+ indexRowsByName.clear();
9172
+ docRowsById.clear();
9173
+ activeIndexRow = null;
9174
+ activeDocRow = null;
9109
9175
  docMoreBtn.hidden = true;
9110
9176
  mappingBody.innerHTML = "";
9111
9177
  mappingBody.textContent = "Select an index to view its mapping.";
@@ -9396,6 +9462,7 @@ ${frontmatter.yaml}
9396
9462
  if (disposed)
9397
9463
  return;
9398
9464
  const dbId = deps.getDbId();
9465
+ const schema = deps.getSchema();
9399
9466
  if (!dbId)
9400
9467
  return;
9401
9468
  const term = input.value.trim();
@@ -9416,6 +9483,7 @@ ${frontmatter.yaml}
9416
9483
  },
9417
9484
  body: JSON.stringify({
9418
9485
  db: dbId,
9486
+ ...schema ? { schema } : {},
9419
9487
  term,
9420
9488
  includeNonText: nonTextCheck.checked
9421
9489
  })
@@ -9494,16 +9562,17 @@ ${frontmatter.yaml}
9494
9562
  return;
9495
9563
  const grouped = new Map;
9496
9564
  for (const h of hits) {
9497
- const existing = grouped.get(h.table) || [];
9565
+ const key = h.schema ? `${h.schema}.${h.table}` : h.table;
9566
+ const existing = grouped.get(key) || [];
9498
9567
  existing.push(h);
9499
- grouped.set(h.table, existing);
9568
+ grouped.set(key, existing);
9500
9569
  }
9501
- for (const [table2, tableHits] of grouped) {
9570
+ for (const [tableLabel, tableHits] of grouped) {
9502
9571
  const section = document.createElement("div");
9503
9572
  section.className = "db-search-table-section";
9504
9573
  const tableHeader = document.createElement("div");
9505
9574
  tableHeader.className = "db-search-table-header";
9506
- tableHeader.textContent = `${table2} (${tableHits.length}件)`;
9575
+ tableHeader.textContent = `${tableLabel} (${tableHits.length}件)`;
9507
9576
  section.appendChild(tableHeader);
9508
9577
  const hitsList = document.createElement("div");
9509
9578
  hitsList.className = "db-search-hits-list";
@@ -9950,6 +10019,22 @@ ${frontmatter.yaml}
9950
10019
  let entries = [];
9951
10020
  const expandedIds = new Set;
9952
10021
  let clearConfirmTimer = null;
10022
+ let lastRefreshKey = null;
10023
+ let lastRefreshAt = 0;
10024
+ let inFlightRefresh = null;
10025
+ const entryRowsById = new Map;
10026
+ let selectedEntryRow = null;
10027
+ function currentRefreshParams() {
10028
+ const dbId = callbacks.getDbId();
10029
+ const schema = callbacks.getSchema();
10030
+ const searchParams = new URLSearchParams;
10031
+ if (dbId)
10032
+ searchParams.set("db", dbId);
10033
+ if (schema)
10034
+ searchParams.set("schema", schema);
10035
+ const params = searchParams.toString() ? `?${searchParams.toString()}` : "";
10036
+ return { key: params, params };
10037
+ }
9953
10038
  function armButtonConfirm(button, confirmText, defaultText) {
9954
10039
  if (button.dataset.confirm === "1") {
9955
10040
  button.dataset.confirm = "";
@@ -9966,23 +10051,42 @@ ${frontmatter.yaml}
9966
10051
  }, 3000);
9967
10052
  return false;
9968
10053
  }
9969
- async function refresh() {
9970
- const dbId = callbacks.getDbId();
9971
- const params = dbId ? `?db=${encodeURIComponent(dbId)}` : "";
9972
- try {
10054
+ async function refresh(options = {}) {
10055
+ const { key: refreshKey, params } = currentRefreshParams();
10056
+ const now = Date.now();
10057
+ if (inFlightRefresh?.key === refreshKey) {
10058
+ return inFlightRefresh.promise;
10059
+ }
10060
+ if (!options.force && refreshKey === lastRefreshKey && now - lastRefreshAt < 1000) {
10061
+ return;
10062
+ }
10063
+ const promise = (async () => {
9973
10064
  const res = await fetch(`/_db/history${params}`);
9974
10065
  if (!res.ok)
9975
10066
  return;
9976
10067
  const state = await res.json();
10068
+ if (currentRefreshParams().key !== refreshKey)
10069
+ return;
9977
10070
  entries = state.entries;
10071
+ lastRefreshKey = refreshKey;
10072
+ lastRefreshAt = Date.now();
9978
10073
  if (selectedEntryId && !entries.some((entry) => entry.id === selectedEntryId)) {
9979
10074
  clearDetail();
9980
10075
  }
9981
10076
  render();
9982
- } catch {}
10077
+ })().catch(() => {});
10078
+ inFlightRefresh = { key: refreshKey, promise };
10079
+ try {
10080
+ await promise;
10081
+ } finally {
10082
+ if (inFlightRefresh?.promise === promise)
10083
+ inFlightRefresh = null;
10084
+ }
9983
10085
  }
9984
10086
  function render() {
9985
10087
  listEl.innerHTML = "";
10088
+ entryRowsById.clear();
10089
+ selectedEntryRow = null;
9986
10090
  if (entries.length === 0) {
9987
10091
  const empty = document.createElement("div");
9988
10092
  empty.className = "db-query-history-empty";
@@ -9990,21 +10094,25 @@ ${frontmatter.yaml}
9990
10094
  listEl.appendChild(empty);
9991
10095
  return;
9992
10096
  }
10097
+ const fragment = document.createDocumentFragment();
9993
10098
  for (const entry of entries) {
9994
- listEl.appendChild(renderEntry(entry));
10099
+ fragment.appendChild(renderEntry(entry));
9995
10100
  }
10101
+ listEl.appendChild(fragment);
9996
10102
  }
9997
10103
  let selectedEntryId = null;
9998
10104
  function clearDetail() {
9999
10105
  selectedEntryId = null;
10106
+ selectedEntryRow?.classList.remove("selected");
10107
+ selectedEntryRow = null;
10000
10108
  detailCol.innerHTML = "";
10001
10109
  detailCol.appendChild(detailPlaceholder);
10002
10110
  }
10003
10111
  function selectEntry(entry) {
10004
10112
  selectedEntryId = entry.id;
10005
- listEl.querySelectorAll(".db-query-history-entry").forEach((el2) => {
10006
- el2.classList.toggle("selected", el2.dataset.id === entry.id);
10007
- });
10113
+ selectedEntryRow?.classList.remove("selected");
10114
+ selectedEntryRow = entryRowsById.get(entry.id) ?? null;
10115
+ selectedEntryRow?.classList.add("selected");
10008
10116
  renderDetail(entry);
10009
10117
  }
10010
10118
  function renderDetail(entry) {
@@ -10061,9 +10169,12 @@ ${frontmatter.yaml}
10061
10169
  function renderEntry(entry) {
10062
10170
  const item = document.createElement("div");
10063
10171
  item.className = "db-query-history-entry";
10064
- if (entry.id === selectedEntryId)
10172
+ if (entry.id === selectedEntryId) {
10065
10173
  item.classList.add("selected");
10174
+ selectedEntryRow = item;
10175
+ }
10066
10176
  item.dataset.id = entry.id;
10177
+ entryRowsById.set(entry.id, item);
10067
10178
  const meta = document.createElement("div");
10068
10179
  meta.className = "db-query-history-entry-meta";
10069
10180
  const byIcon = document.createElement("span");
@@ -10150,8 +10261,9 @@ ${frontmatter.yaml}
10150
10261
  expandedIds.delete(id);
10151
10262
  if (selectedEntryId === id)
10152
10263
  resetDetailCol();
10153
- const itemEl = listEl.querySelector(`.db-query-history-entry[data-id="${CSS.escape(id)}"]`);
10264
+ const itemEl = entryRowsById.get(id);
10154
10265
  if (itemEl) {
10266
+ entryRowsById.delete(id);
10155
10267
  itemEl.remove();
10156
10268
  if (entries.length === 0)
10157
10269
  render();
@@ -10181,24 +10293,27 @@ ${frontmatter.yaml}
10181
10293
  clearConfirmTimer = null;
10182
10294
  }
10183
10295
  try {
10296
+ const schema = callbacks.getSchema();
10184
10297
  await fetch("/_db/history/clear", {
10185
10298
  method: "POST",
10186
10299
  headers: {
10187
10300
  "Content-Type": "application/json",
10188
10301
  "X-Code-Viewer-Action": "1"
10189
10302
  },
10190
- body: JSON.stringify(dbId ? { db: dbId } : {})
10303
+ body: JSON.stringify(dbId ? { db: dbId, ...schema ? { schema } : {} } : {})
10191
10304
  });
10192
10305
  entries = [];
10193
10306
  render();
10194
10307
  } catch {}
10195
10308
  });
10196
10309
  refreshBtn.addEventListener("click", () => {
10197
- refresh();
10310
+ refresh({ force: true });
10198
10311
  });
10199
10312
  function clear() {
10200
10313
  entries = [];
10201
10314
  expandedIds.clear();
10315
+ lastRefreshKey = null;
10316
+ lastRefreshAt = 0;
10202
10317
  if (clearConfirmTimer) {
10203
10318
  clearTimeout(clearConfirmTimer);
10204
10319
  clearConfirmTimer = null;
@@ -10300,6 +10415,10 @@ ${frontmatter.yaml}
10300
10415
  let keyRunId = 0;
10301
10416
  let suppressNotify = false;
10302
10417
  let disposed = false;
10418
+ let activeDbRow = null;
10419
+ let activeKeyRow = null;
10420
+ const dbRowsByIndex = new Map;
10421
+ const keyRowsByName = new Map;
10303
10422
  const dbGuard = createAbortGuard();
10304
10423
  const keysGuard = createAbortGuard();
10305
10424
  const valueGuard = createAbortGuard();
@@ -10325,10 +10444,13 @@ ${frontmatter.yaml}
10325
10444
  }
10326
10445
  function renderDatabases(databases) {
10327
10446
  dbList.innerHTML = "";
10447
+ dbRowsByIndex.clear();
10448
+ const fragment = document.createDocumentFragment();
10328
10449
  for (const db of databases) {
10329
10450
  const item = document.createElement("div");
10330
10451
  item.className = "redis-db-item";
10331
10452
  item.dataset.dbIndex = String(db.index);
10453
+ dbRowsByIndex.set(db.index, item);
10332
10454
  const name = document.createElement("span");
10333
10455
  name.className = "redis-db-name";
10334
10456
  name.textContent = `db${db.index}`;
@@ -10336,20 +10458,24 @@ ${frontmatter.yaml}
10336
10458
  count.className = "redis-db-count";
10337
10459
  count.textContent = `${db.keyCount.toLocaleString()} keys`;
10338
10460
  item.append(name, count);
10339
- item.addEventListener("click", () => selectDatabase(db.index));
10340
- dbList.appendChild(item);
10461
+ fragment.appendChild(item);
10341
10462
  }
10463
+ dbList.appendChild(fragment);
10342
10464
  }
10343
10465
  function highlightActiveDb(index) {
10344
- for (const item of dbList.querySelectorAll(".redis-db-item")) {
10345
- item.classList.toggle("active", item.dataset.dbIndex === String(index));
10346
- }
10466
+ if (activeDbRow?.dataset.dbIndex === String(index))
10467
+ return;
10468
+ activeDbRow?.classList.remove("active");
10469
+ activeDbRow = dbRowsByIndex.get(index) ?? null;
10470
+ activeDbRow?.classList.add("active");
10347
10471
  }
10348
10472
  function appendKeys(keys) {
10473
+ const fragment = document.createDocumentFragment();
10349
10474
  for (const k of keys) {
10350
10475
  const row = document.createElement("div");
10351
10476
  row.className = "redis-key-item";
10352
10477
  row.dataset.keyName = k.name;
10478
+ keyRowsByName.set(k.name, row);
10353
10479
  const typeBadge = document.createElement("span");
10354
10480
  typeBadge.className = `redis-type-badge redis-type-${k.type}`;
10355
10481
  typeBadge.textContent = k.type;
@@ -10358,18 +10484,20 @@ ${frontmatter.yaml}
10358
10484
  nameEl.textContent = k.name;
10359
10485
  nameEl.title = k.name;
10360
10486
  row.append(typeBadge, nameEl);
10361
- row.addEventListener("click", () => selectKey(k.name));
10362
- keyList.appendChild(row);
10487
+ fragment.appendChild(row);
10363
10488
  }
10489
+ keyList.appendChild(fragment);
10364
10490
  }
10365
10491
  function highlightActiveKey(name) {
10366
- for (const row of keyList.querySelectorAll(".redis-key-item")) {
10367
- row.classList.toggle("active", row.dataset.keyName === name);
10368
- }
10492
+ if (activeKeyRow?.dataset.keyName === name)
10493
+ return;
10494
+ activeKeyRow?.classList.remove("active");
10495
+ activeKeyRow = keyRowsByName.get(name) ?? null;
10496
+ activeKeyRow?.classList.add("active");
10369
10497
  }
10370
10498
  function makeNotice(message, kind = "info") {
10371
10499
  const div = document.createElement("div");
10372
- div.className = kind === "warn" ? "redis-value-truncation" : "redis-value-info";
10500
+ div.className = kind === "warn" ? "datastore-value-truncation" : "datastore-value-info";
10373
10501
  div.textContent = message;
10374
10502
  return div;
10375
10503
  }
@@ -10527,6 +10655,8 @@ ${frontmatter.yaml}
10527
10655
  notifySelectionChange();
10528
10656
  highlightActiveDb(dbIndex);
10529
10657
  keyList.innerHTML = "";
10658
+ keyRowsByName.clear();
10659
+ activeKeyRow = null;
10530
10660
  mainPane.textContent = "Select a key to view its value.";
10531
10661
  await loadKeys(false);
10532
10662
  }
@@ -10562,8 +10692,11 @@ ${frontmatter.yaml}
10562
10692
  if (disposed || slot.isStale() || requestRunId !== loadRunId || requestDbId !== currentDbId || requestDbIndex !== currentDbIndex || data.dbIndex !== requestDbIndex) {
10563
10693
  return;
10564
10694
  }
10565
- if (!append)
10695
+ if (!append) {
10566
10696
  keyList.innerHTML = "";
10697
+ keyRowsByName.clear();
10698
+ activeKeyRow = null;
10699
+ }
10567
10700
  if (data.keys.length === 0 && !append) {
10568
10701
  setKeyStatus("(no keys)");
10569
10702
  } else {
@@ -10584,6 +10717,23 @@ ${frontmatter.yaml}
10584
10717
  }
10585
10718
  }
10586
10719
  keyMoreBtn.addEventListener("click", () => loadKeys(true));
10720
+ dbList.addEventListener("click", (e2) => {
10721
+ const row = e2.target?.closest(".redis-db-item");
10722
+ if (!row || !dbList.contains(row))
10723
+ return;
10724
+ const raw = row.dataset.dbIndex;
10725
+ const index = raw === undefined ? NaN : Number(raw);
10726
+ if (Number.isInteger(index))
10727
+ selectDatabase(index);
10728
+ });
10729
+ keyList.addEventListener("click", (e2) => {
10730
+ const row = e2.target?.closest(".redis-key-item");
10731
+ if (!row || !keyList.contains(row))
10732
+ return;
10733
+ const key = row.dataset.keyName;
10734
+ if (key)
10735
+ selectKey(key);
10736
+ });
10587
10737
  keyFilterForm.addEventListener("submit", (e2) => {
10588
10738
  e2.preventDefault();
10589
10739
  const nextFilter = keyFilterInput.value.trim() || "*";
@@ -10613,6 +10763,9 @@ ${frontmatter.yaml}
10613
10763
  keyFilterInput.value = currentKeyFilter === "*" ? "" : currentKeyFilter;
10614
10764
  currentCursor = "0";
10615
10765
  keyList.innerHTML = "";
10766
+ keyRowsByName.clear();
10767
+ activeDbRow = null;
10768
+ activeKeyRow = null;
10616
10769
  keyMoreBtn.hidden = true;
10617
10770
  mainPane.textContent = "Select a key to view its value.";
10618
10771
  setDbStatus("Loading databases...");
@@ -10632,11 +10785,21 @@ ${frontmatter.yaml}
10632
10785
  if (initial?.dbIndex !== undefined && data.databases.some((d2) => d2.index === initial.dbIndex)) {
10633
10786
  suppressNotify = true;
10634
10787
  try {
10635
- await selectDatabase(initial.dbIndex);
10788
+ currentDbIndex = initial.dbIndex;
10789
+ currentKey = null;
10790
+ currentCursor = "0";
10791
+ highlightActiveDb(initial.dbIndex);
10792
+ keyList.innerHTML = "";
10793
+ keyRowsByName.clear();
10794
+ mainPane.textContent = "Select a key to view its value.";
10795
+ const valuePromise = initial.key ? selectKey(initial.key).catch(() => {}) : null;
10796
+ await loadKeys(false);
10636
10797
  if (currentDbId !== dbId)
10637
10798
  return;
10638
- if (initial.key) {
10639
- await selectKey(initial.key);
10799
+ if (valuePromise) {
10800
+ await valuePromise;
10801
+ if (initial.key)
10802
+ highlightActiveKey(initial.key);
10640
10803
  }
10641
10804
  } finally {
10642
10805
  suppressNotify = false;
@@ -10666,6 +10829,10 @@ ${frontmatter.yaml}
10666
10829
  currentCursor = "0";
10667
10830
  dbList.innerHTML = "";
10668
10831
  keyList.innerHTML = "";
10832
+ dbRowsByIndex.clear();
10833
+ keyRowsByName.clear();
10834
+ activeDbRow = null;
10835
+ activeKeyRow = null;
10669
10836
  keyMoreBtn.hidden = true;
10670
10837
  mainPane.textContent = "Select a database to view keys.";
10671
10838
  }
@@ -10683,6 +10850,654 @@ ${frontmatter.yaml}
10683
10850
  return { el: container, load, clear, dispose, getSelection };
10684
10851
  }
10685
10852
 
10853
+ // web-src/core/database/s3-keys.ts
10854
+ function s3ObjectName(key) {
10855
+ const trimmed = key.replace(/\/+$/, "");
10856
+ const idx = trimmed.lastIndexOf("/");
10857
+ return idx >= 0 ? trimmed.slice(idx + 1) : trimmed;
10858
+ }
10859
+
10860
+ // web-src/views/source-preview-elements.ts
10861
+ function renderHtmlPreviewFrame(title, html, extraClass = "") {
10862
+ const preview = document.createElement("div");
10863
+ preview.className = ["gdp-html-preview", extraClass].filter(Boolean).join(" ");
10864
+ const frame = document.createElement("iframe");
10865
+ frame.title = title;
10866
+ frame.sandbox.value = "";
10867
+ frame.referrerPolicy = "no-referrer";
10868
+ frame.srcdoc = html;
10869
+ preview.appendChild(frame);
10870
+ return preview;
10871
+ }
10872
+ function appendMediaEmbed(view, opts) {
10873
+ if (opts.kind === "video") {
10874
+ const video = document.createElement("video");
10875
+ video.src = opts.url;
10876
+ video.controls = true;
10877
+ video.preload = "metadata";
10878
+ view.appendChild(video);
10879
+ } else if (opts.kind === "audio") {
10880
+ const audio = document.createElement("audio");
10881
+ audio.src = opts.url;
10882
+ audio.controls = true;
10883
+ audio.preload = "metadata";
10884
+ view.appendChild(audio);
10885
+ } else if (opts.kind === "pdf") {
10886
+ const frame = document.createElement("iframe");
10887
+ frame.src = opts.url;
10888
+ frame.title = opts.title;
10889
+ frame.loading = "lazy";
10890
+ view.appendChild(frame);
10891
+ } else {
10892
+ const img = document.createElement("img");
10893
+ img.src = opts.url;
10894
+ img.alt = "";
10895
+ if (opts.onImageLoad) {
10896
+ img.addEventListener("load", () => opts.onImageLoad?.(img), {
10897
+ once: true
10898
+ });
10899
+ }
10900
+ view.appendChild(img);
10901
+ }
10902
+ }
10903
+ function renderUnsupportedPreview(opts) {
10904
+ const view = document.createElement("div");
10905
+ view.className = ["gdp-source-viewer unsupported", opts.className || ""].filter(Boolean).join(" ");
10906
+ const content = document.createElement("div");
10907
+ content.className = "gdp-source-unsupported-content";
10908
+ const title = document.createElement("strong");
10909
+ title.className = "gdp-source-unsupported-title";
10910
+ title.textContent = "Preview unavailable";
10911
+ const message = document.createElement("div");
10912
+ message.className = "gdp-source-unsupported-message";
10913
+ message.textContent = opts.message;
10914
+ content.append(title, message, ...opts.extraChildren || []);
10915
+ view.appendChild(content);
10916
+ return view;
10917
+ }
10918
+
10919
+ // web-src/views/database/s3-explorer.ts
10920
+ function buildS3RawUrl(dbId, bucket, key) {
10921
+ const params = new URLSearchParams({ db: dbId, bucket, key });
10922
+ return `/_db/s3/raw?${params}`;
10923
+ }
10924
+ function s3Uri(bucket, key) {
10925
+ return `s3://${bucket}/${key}`;
10926
+ }
10927
+ function objectTypeLabel(key, contentType) {
10928
+ const kind = sourceDisplayKind(key);
10929
+ return humanFileKind(key, contentType, kind === "unsupported" ? "unsupported file" : kind);
10930
+ }
10931
+ function createS3Explorer(callbacks = {}) {
10932
+ const container = document.createElement("div");
10933
+ container.className = "s3-explorer";
10934
+ const objectPane = document.createElement("div");
10935
+ objectPane.className = "s3-object-list-pane";
10936
+ const toolbar = document.createElement("div");
10937
+ toolbar.className = "s3-toolbar";
10938
+ const bucketRow = document.createElement("div");
10939
+ bucketRow.className = "s3-bucket-row";
10940
+ const bucketLabel = document.createElement("label");
10941
+ bucketLabel.className = "s3-field-label";
10942
+ bucketLabel.textContent = "Bucket";
10943
+ const bucketSelect = document.createElement("select");
10944
+ bucketSelect.className = "s3-bucket-select";
10945
+ bucketLabel.appendChild(bucketSelect);
10946
+ bucketRow.appendChild(bucketLabel);
10947
+ const searchRow = document.createElement("form");
10948
+ searchRow.className = "s3-search-row";
10949
+ const searchInput = document.createElement("input");
10950
+ searchInput.type = "search";
10951
+ searchInput.className = "s3-search-input";
10952
+ searchInput.placeholder = "Search objects";
10953
+ searchInput.autocomplete = "off";
10954
+ const searchBtn = document.createElement("button");
10955
+ searchBtn.type = "submit";
10956
+ searchBtn.className = "db-btn db-btn-primary s3-search-btn";
10957
+ searchBtn.textContent = "Search";
10958
+ searchRow.append(searchInput, searchBtn);
10959
+ const optionRow = document.createElement("div");
10960
+ optionRow.className = "s3-options-row";
10961
+ const modeSeg = document.createElement("div");
10962
+ modeSeg.className = "seg s3-mode-seg";
10963
+ const prefixModeBtn = document.createElement("button");
10964
+ prefixModeBtn.type = "button";
10965
+ prefixModeBtn.textContent = "Prefix";
10966
+ const containsModeBtn = document.createElement("button");
10967
+ containsModeBtn.type = "button";
10968
+ containsModeBtn.textContent = "Contains";
10969
+ modeSeg.append(prefixModeBtn, containsModeBtn);
10970
+ const sortSelect = document.createElement("select");
10971
+ sortSelect.className = "s3-sort-select";
10972
+ const sortUpdated = document.createElement("option");
10973
+ sortUpdated.value = "updated-desc";
10974
+ sortUpdated.textContent = "Updated newest";
10975
+ const sortKey = document.createElement("option");
10976
+ sortKey.value = "key-asc";
10977
+ sortKey.textContent = "Key A-Z";
10978
+ sortSelect.append(sortUpdated, sortKey);
10979
+ optionRow.append(modeSeg, sortSelect);
10980
+ toolbar.append(bucketRow, searchRow, optionRow);
10981
+ objectPane.appendChild(toolbar);
10982
+ const objectStatus = document.createElement("div");
10983
+ objectStatus.className = "s3-object-status";
10984
+ objectPane.appendChild(objectStatus);
10985
+ const objectList = document.createElement("div");
10986
+ objectList.className = "s3-object-list";
10987
+ objectPane.appendChild(objectList);
10988
+ const moreBtn = document.createElement("button");
10989
+ moreBtn.type = "button";
10990
+ moreBtn.className = "s3-object-more-btn";
10991
+ moreBtn.textContent = "Load more";
10992
+ moreBtn.hidden = true;
10993
+ objectPane.appendChild(moreBtn);
10994
+ const previewPane = document.createElement("div");
10995
+ previewPane.className = "s3-preview-pane";
10996
+ previewPane.textContent = "Select an object to preview.";
10997
+ container.append(objectPane, previewPane);
10998
+ let currentDbId = null;
10999
+ let currentBucket = null;
11000
+ let currentKey = null;
11001
+ let currentMode = "prefix";
11002
+ let currentSort = "updated-desc";
11003
+ let currentSearch = "";
11004
+ let currentNextToken;
11005
+ const objectsByKey = new Map;
11006
+ const objectRowsByKey = new Map;
11007
+ let disposed = false;
11008
+ let loadRunId = 0;
11009
+ let objectRunId = 0;
11010
+ let suppressNotify = false;
11011
+ let activeObjectRow = null;
11012
+ const bucketGuard = createAbortGuard();
11013
+ const objectGuard = createAbortGuard();
11014
+ const previewGuard = createAbortGuard();
11015
+ function notifySelectionChange() {
11016
+ if (suppressNotify)
11017
+ return;
11018
+ callbacks.onSelectionChange?.(getSelection());
11019
+ }
11020
+ function setMode(mode) {
11021
+ currentMode = mode;
11022
+ prefixModeBtn.classList.toggle("active", mode === "prefix");
11023
+ containsModeBtn.classList.toggle("active", mode === "contains");
11024
+ searchInput.placeholder = mode === "prefix" ? "Prefix, e.g. photos/2026/" : "Filename contains";
11025
+ }
11026
+ function renderBuckets(buckets) {
11027
+ bucketSelect.innerHTML = "";
11028
+ for (const bucket of buckets) {
11029
+ const opt = document.createElement("option");
11030
+ opt.value = bucket.name;
11031
+ opt.textContent = bucket.name;
11032
+ bucketSelect.appendChild(opt);
11033
+ }
11034
+ bucketSelect.disabled = buckets.length === 0;
11035
+ }
11036
+ function setObjectStatusFromResponse(resp) {
11037
+ objectStatus.textContent = "";
11038
+ const parts = [
11039
+ `${resp.objects.length.toLocaleString()} shown`,
11040
+ `${resp.scannedObjects.toLocaleString()} scanned`,
11041
+ resp.sort === "updated-desc" ? "newest first in scanned objects" : "sorted by key"
11042
+ ];
11043
+ if (resp.scanLimitReached) {
11044
+ parts.push("scan cap reached; narrow the prefix to search more precisely");
11045
+ }
11046
+ objectStatus.textContent = parts.join(" / ");
11047
+ objectStatus.classList.toggle("warn", !!resp.scanLimitReached);
11048
+ }
11049
+ function highlightActiveObject(key) {
11050
+ if (activeObjectRow?.dataset.key === key)
11051
+ return;
11052
+ activeObjectRow?.classList.remove("active");
11053
+ activeObjectRow = key ? objectRowsByKey.get(key) ?? null : null;
11054
+ activeObjectRow?.classList.add("active");
11055
+ }
11056
+ async function fetchObjectHeadForSelection(key, signal) {
11057
+ if (!currentDbId || !currentBucket)
11058
+ return { key, sizeBytes: 0 };
11059
+ const params = new URLSearchParams({
11060
+ db: currentDbId,
11061
+ bucket: currentBucket,
11062
+ key
11063
+ });
11064
+ const res = await fetch(`/_db/s3/head?${params}`, { signal });
11065
+ if (!res.ok)
11066
+ return { key, sizeBytes: 0 };
11067
+ const data = await res.json();
11068
+ return {
11069
+ key,
11070
+ sizeBytes: data.sizeBytes ?? 0,
11071
+ ...data.updatedAt ? { updatedAt: data.updatedAt } : {},
11072
+ ...data.contentType ? { contentType: data.contentType } : {},
11073
+ ...data.etag ? { etag: data.etag } : {}
11074
+ };
11075
+ }
11076
+ function appendObjects(objects) {
11077
+ const fragment = document.createDocumentFragment();
11078
+ for (const object of objects) {
11079
+ objectsByKey.set(object.key, object);
11080
+ const row = document.createElement("div");
11081
+ row.className = "s3-object-item";
11082
+ row.dataset.key = object.key;
11083
+ objectRowsByKey.set(object.key, row);
11084
+ const name = document.createElement("span");
11085
+ name.className = "s3-object-name";
11086
+ name.textContent = object.key;
11087
+ name.title = object.key;
11088
+ const meta = document.createElement("span");
11089
+ meta.className = "s3-object-meta";
11090
+ const kind = objectTypeLabel(object.key, object.contentType);
11091
+ const size = formatBytes(object.sizeBytes);
11092
+ const updated = formatFileDate(object.updatedAt);
11093
+ meta.textContent = [kind, size, updated].filter(Boolean).join(" / ");
11094
+ row.append(name, meta);
11095
+ fragment.appendChild(row);
11096
+ }
11097
+ objectList.appendChild(fragment);
11098
+ }
11099
+ async function loadObjects(append) {
11100
+ if (!currentDbId || !currentBucket || disposed)
11101
+ return;
11102
+ const slot = objectGuard.start();
11103
+ const requestRunId = loadRunId;
11104
+ const requestDbId = currentDbId;
11105
+ const requestBucket = currentBucket;
11106
+ const requestSearch = currentSearch;
11107
+ const requestMode = currentMode;
11108
+ const requestSort = currentSort;
11109
+ moreBtn.disabled = true;
11110
+ if (!append) {
11111
+ objectList.innerHTML = "";
11112
+ objectsByKey.clear();
11113
+ objectRowsByKey.clear();
11114
+ activeObjectRow = null;
11115
+ currentKey = null;
11116
+ currentNextToken = undefined;
11117
+ setPaneStatus(objectList, "Loading objects...");
11118
+ previewPane.textContent = "Select an object to preview.";
11119
+ }
11120
+ try {
11121
+ const params = new URLSearchParams({
11122
+ db: requestDbId,
11123
+ bucket: requestBucket,
11124
+ mode: requestMode,
11125
+ sort: requestSort,
11126
+ limit: requestSort === "updated-desc" ? "1000" : "200"
11127
+ });
11128
+ if (requestMode === "prefix") {
11129
+ if (requestSearch)
11130
+ params.set("q", requestSearch);
11131
+ } else if (requestSearch) {
11132
+ params.set("q", requestSearch);
11133
+ }
11134
+ if (append && currentNextToken)
11135
+ params.set("token", currentNextToken);
11136
+ const res = await fetch(`/_db/s3/objects?${params}`, {
11137
+ signal: slot.signal
11138
+ });
11139
+ if (disposed || slot.isStale())
11140
+ return;
11141
+ if (!res.ok) {
11142
+ const text2 = await res.text();
11143
+ setPaneStatus(objectList, `Error: ${text2 || res.statusText}`, {
11144
+ error: true
11145
+ });
11146
+ objectStatus.textContent = "";
11147
+ return;
11148
+ }
11149
+ const data = await res.json();
11150
+ if (disposed || slot.isStale() || requestRunId !== loadRunId || requestDbId !== currentDbId || requestBucket !== currentBucket || requestSearch !== currentSearch || requestMode !== currentMode || requestSort !== currentSort) {
11151
+ return;
11152
+ }
11153
+ if (!append)
11154
+ objectList.innerHTML = "";
11155
+ if (data.objects.length === 0 && !append) {
11156
+ setPaneStatus(objectList, data.scanLimitReached ? `(no matches in the first ${data.scannedObjects.toLocaleString()} scanned objects; narrow the prefix and search again)` : "(no objects)");
11157
+ } else {
11158
+ appendObjects(data.objects);
11159
+ }
11160
+ currentNextToken = data.nextToken;
11161
+ moreBtn.hidden = !data.nextToken;
11162
+ setObjectStatusFromResponse(data);
11163
+ highlightActiveObject(currentKey);
11164
+ } catch (err) {
11165
+ if (slot.isStale())
11166
+ return;
11167
+ setPaneStatus(objectList, `Error: ${err instanceof Error ? err.message : String(err)}`, { error: true });
11168
+ objectStatus.textContent = "";
11169
+ } finally {
11170
+ slot.finish();
11171
+ if (!slot.isStale())
11172
+ moreBtn.disabled = false;
11173
+ }
11174
+ }
11175
+ function renderPreviewHeader(object) {
11176
+ const header = document.createElement("div");
11177
+ header.className = "s3-preview-header";
11178
+ const title = document.createElement("div");
11179
+ title.className = "s3-preview-title";
11180
+ title.textContent = object.key;
11181
+ title.title = object.key;
11182
+ const meta = document.createElement("div");
11183
+ meta.className = "s3-preview-meta";
11184
+ meta.textContent = [
11185
+ objectTypeLabel(object.key, object.contentType),
11186
+ formatBytes(object.sizeBytes),
11187
+ formatFileDate(object.updatedAt)
11188
+ ].filter(Boolean).join(" / ");
11189
+ const actions = document.createElement("div");
11190
+ actions.className = "s3-preview-actions";
11191
+ if (currentDbId && currentBucket) {
11192
+ const rawUrl = buildS3RawUrl(currentDbId, currentBucket, object.key);
11193
+ const open = document.createElement("a");
11194
+ open.className = "db-btn db-btn-sm";
11195
+ open.href = rawUrl;
11196
+ open.target = "_blank";
11197
+ open.rel = "noreferrer";
11198
+ open.textContent = "Open raw";
11199
+ const download = document.createElement("a");
11200
+ download.className = "db-btn db-btn-sm";
11201
+ download.href = rawUrl;
11202
+ download.download = s3ObjectName(object.key);
11203
+ download.textContent = "Download";
11204
+ const copy = document.createElement("button");
11205
+ copy.type = "button";
11206
+ copy.className = "db-btn db-btn-sm";
11207
+ copy.textContent = "Copy S3 URI";
11208
+ copy.addEventListener("click", async () => {
11209
+ try {
11210
+ await navigator.clipboard.writeText(s3Uri(currentBucket || "", object.key));
11211
+ copy.textContent = "Copied";
11212
+ window.setTimeout(() => {
11213
+ copy.textContent = "Copy S3 URI";
11214
+ }, 1200);
11215
+ } catch {
11216
+ copy.textContent = "Copy failed";
11217
+ }
11218
+ });
11219
+ actions.append(open, download, copy);
11220
+ }
11221
+ header.append(title, meta, actions);
11222
+ return header;
11223
+ }
11224
+ function renderMediaPreview(object, kind) {
11225
+ const view = document.createElement("div");
11226
+ view.className = `gdp-source-viewer media ${kind} s3-source-preview`;
11227
+ if (!currentDbId || !currentBucket)
11228
+ return view;
11229
+ const url = buildS3RawUrl(currentDbId, currentBucket, object.key);
11230
+ appendMediaEmbed(view, { url, kind, title: object.key });
11231
+ return view;
11232
+ }
11233
+ function renderUnsupported() {
11234
+ return renderUnsupportedPreview({
11235
+ className: "s3-source-preview",
11236
+ message: "This object type cannot be previewed safely in the browser."
11237
+ });
11238
+ }
11239
+ async function renderTextPreview(object, slot) {
11240
+ if (!currentDbId || !currentBucket)
11241
+ return null;
11242
+ const params = new URLSearchParams({
11243
+ db: currentDbId,
11244
+ bucket: currentBucket,
11245
+ key: object.key
11246
+ });
11247
+ const res = await fetch(`/_db/s3/text?${params}`, { signal: slot.signal });
11248
+ if (disposed || slot.isStale())
11249
+ return null;
11250
+ if (!res.ok) {
11251
+ const text2 = await res.text();
11252
+ const error2 = document.createElement("div");
11253
+ error2.className = "db-pane-error";
11254
+ error2.textContent = text2 || res.statusText;
11255
+ return error2;
11256
+ }
11257
+ const data = await res.json();
11258
+ if (disposed || slot.isStale())
11259
+ return null;
11260
+ const previewKind = sourcePreviewKind(object.key);
11261
+ let body;
11262
+ if (previewKind === "html") {
11263
+ body = renderHtmlPreviewFrame(`${object.key} preview`, data.text, "s3-html-preview");
11264
+ } else if (previewKind === "markdown") {
11265
+ body = await renderMarkdownPreview(data.text, { path: object.key, ref: "s3" }, {
11266
+ syntaxHighlight: true,
11267
+ signal: slot.signal,
11268
+ resolveAssetUrl: (path) => currentDbId && currentBucket ? buildS3RawUrl(currentDbId, currentBucket, path) : null
11269
+ });
11270
+ } else {
11271
+ const pre = document.createElement("pre");
11272
+ pre.className = "s3-text-preview";
11273
+ pre.textContent = data.text;
11274
+ body = pre;
11275
+ }
11276
+ if (data.truncated) {
11277
+ const wrap = document.createElement("div");
11278
+ wrap.className = "s3-text-preview-wrap";
11279
+ const note = document.createElement("div");
11280
+ note.className = "datastore-value-truncation";
11281
+ note.textContent = `Showing first ${formatBytes(512 * 1024)}.`;
11282
+ wrap.append(note, body);
11283
+ return wrap;
11284
+ }
11285
+ return body;
11286
+ }
11287
+ async function selectObject(object) {
11288
+ if (disposed || !currentDbId || !currentBucket)
11289
+ return;
11290
+ const slot = previewGuard.start();
11291
+ const requestRunId = ++objectRunId;
11292
+ const requestDbId = currentDbId;
11293
+ const requestBucket = currentBucket;
11294
+ currentKey = object.key;
11295
+ notifySelectionChange();
11296
+ highlightActiveObject(object.key);
11297
+ previewPane.innerHTML = "";
11298
+ previewPane.appendChild(renderPreviewHeader(object));
11299
+ const body = document.createElement("div");
11300
+ body.className = "s3-preview-body";
11301
+ setPaneStatus(body, "Loading preview...");
11302
+ previewPane.appendChild(body);
11303
+ try {
11304
+ const displayKind = sourceDisplayKind(object.key);
11305
+ let preview;
11306
+ if (displayKind === "image" || displayKind === "video" || displayKind === "audio" || displayKind === "pdf") {
11307
+ preview = renderMediaPreview(object, displayKind);
11308
+ } else if (displayKind === "text") {
11309
+ preview = await renderTextPreview(object, slot);
11310
+ } else {
11311
+ preview = renderUnsupported();
11312
+ }
11313
+ if (disposed || slot.isStale() || requestRunId !== objectRunId || requestDbId !== currentDbId || requestBucket !== currentBucket || object.key !== currentKey) {
11314
+ return;
11315
+ }
11316
+ body.innerHTML = "";
11317
+ if (preview)
11318
+ body.appendChild(preview);
11319
+ } catch (err) {
11320
+ if (slot.isStale())
11321
+ return;
11322
+ setPaneStatus(body, `Error: ${err instanceof Error ? err.message : String(err)}`, { error: true });
11323
+ } finally {
11324
+ slot.finish();
11325
+ }
11326
+ }
11327
+ async function selectBucket(bucket) {
11328
+ if (disposed || !currentDbId)
11329
+ return;
11330
+ currentBucket = bucket;
11331
+ currentKey = null;
11332
+ currentNextToken = undefined;
11333
+ notifySelectionChange();
11334
+ await loadObjects(false);
11335
+ }
11336
+ bucketSelect.addEventListener("change", () => {
11337
+ if (bucketSelect.value)
11338
+ selectBucket(bucketSelect.value);
11339
+ });
11340
+ prefixModeBtn.addEventListener("click", () => {
11341
+ setMode("prefix");
11342
+ notifySelectionChange();
11343
+ loadObjects(false);
11344
+ });
11345
+ containsModeBtn.addEventListener("click", () => {
11346
+ setMode("contains");
11347
+ notifySelectionChange();
11348
+ loadObjects(false);
11349
+ });
11350
+ sortSelect.addEventListener("change", () => {
11351
+ currentSort = sortSelect.value === "key-asc" ? "key-asc" : "updated-desc";
11352
+ notifySelectionChange();
11353
+ loadObjects(false);
11354
+ });
11355
+ searchRow.addEventListener("submit", (e2) => {
11356
+ e2.preventDefault();
11357
+ currentSearch = searchInput.value.trim();
11358
+ notifySelectionChange();
11359
+ loadObjects(false);
11360
+ });
11361
+ searchInput.addEventListener("keydown", (e2) => {
11362
+ if (isImeComposing(e2))
11363
+ return;
11364
+ if (e2.key === "Escape") {
11365
+ searchInput.value = "";
11366
+ currentSearch = "";
11367
+ notifySelectionChange();
11368
+ loadObjects(false);
11369
+ }
11370
+ });
11371
+ moreBtn.addEventListener("click", () => loadObjects(true));
11372
+ objectList.addEventListener("click", (e2) => {
11373
+ const row = e2.target?.closest(".s3-object-item");
11374
+ if (!row || !objectList.contains(row))
11375
+ return;
11376
+ const key = row.dataset.key;
11377
+ if (!key)
11378
+ return;
11379
+ const object = objectsByKey.get(key);
11380
+ if (object)
11381
+ selectObject(object);
11382
+ });
11383
+ async function load(dbId, initial) {
11384
+ if (disposed)
11385
+ return;
11386
+ if (currentDbId === dbId && !initial)
11387
+ return;
11388
+ bucketGuard.dispose();
11389
+ objectGuard.dispose();
11390
+ previewGuard.dispose();
11391
+ const slot = bucketGuard.start();
11392
+ const requestRunId = ++loadRunId;
11393
+ const initialMode = initial?.mode === "contains" ? "contains" : "prefix";
11394
+ currentDbId = dbId;
11395
+ currentBucket = null;
11396
+ currentKey = null;
11397
+ currentSearch = initialMode === "contains" ? initial?.query ?? "" : initial?.prefix ?? "";
11398
+ currentSort = initial?.sort === "key-asc" ? "key-asc" : "updated-desc";
11399
+ setMode(initialMode);
11400
+ sortSelect.value = currentSort;
11401
+ searchInput.value = currentSearch;
11402
+ objectStatus.textContent = "";
11403
+ objectList.innerHTML = "";
11404
+ objectsByKey.clear();
11405
+ objectRowsByKey.clear();
11406
+ activeObjectRow = null;
11407
+ moreBtn.hidden = true;
11408
+ previewPane.textContent = "Select an object to preview.";
11409
+ setPaneStatus(objectList, "Loading buckets...");
11410
+ try {
11411
+ const res = await fetch(`/_db/s3/buckets?db=${encodeURIComponent(dbId)}`, { signal: slot.signal });
11412
+ if (disposed || slot.isStale())
11413
+ return;
11414
+ if (!res.ok) {
11415
+ const text2 = await res.text();
11416
+ setPaneStatus(objectList, `Error: ${text2 || res.statusText}`, {
11417
+ error: true
11418
+ });
11419
+ return;
11420
+ }
11421
+ const data = await res.json();
11422
+ if (disposed || slot.isStale() || requestRunId !== loadRunId || currentDbId !== dbId) {
11423
+ return;
11424
+ }
11425
+ renderBuckets(data.buckets);
11426
+ const selected = initial?.bucket && data.buckets.some((bucket) => bucket.name === initial.bucket) && initial.bucket || data.buckets[0]?.name || null;
11427
+ if (!selected) {
11428
+ setPaneStatus(objectList, "(no buckets)");
11429
+ return;
11430
+ }
11431
+ bucketSelect.value = selected;
11432
+ suppressNotify = true;
11433
+ try {
11434
+ currentBucket = selected;
11435
+ currentKey = null;
11436
+ currentNextToken = undefined;
11437
+ notifySelectionChange();
11438
+ highlightActiveObject(null);
11439
+ const headPromise = initial?.key ? fetchObjectHeadForSelection(initial.key, slot.signal).catch(() => null) : null;
11440
+ await loadObjects(false);
11441
+ if (currentDbId !== dbId)
11442
+ return;
11443
+ if (initial?.key) {
11444
+ const object = objectsByKey.get(initial.key) || (headPromise ? await headPromise : null) || { key: initial.key, sizeBytes: 0 };
11445
+ await selectObject(object);
11446
+ }
11447
+ } finally {
11448
+ suppressNotify = false;
11449
+ }
11450
+ notifySelectionChange();
11451
+ } catch (err) {
11452
+ if (slot.isStale())
11453
+ return;
11454
+ setPaneStatus(objectList, `Error: ${err instanceof Error ? err.message : String(err)}`, { error: true });
11455
+ } finally {
11456
+ slot.finish();
11457
+ }
11458
+ }
11459
+ function clear() {
11460
+ bucketGuard.dispose();
11461
+ objectGuard.dispose();
11462
+ previewGuard.dispose();
11463
+ loadRunId++;
11464
+ objectRunId++;
11465
+ suppressNotify = false;
11466
+ currentDbId = null;
11467
+ currentBucket = null;
11468
+ currentKey = null;
11469
+ currentSearch = "";
11470
+ currentNextToken = undefined;
11471
+ currentSort = "updated-desc";
11472
+ setMode("prefix");
11473
+ sortSelect.value = "updated-desc";
11474
+ searchInput.value = "";
11475
+ bucketSelect.innerHTML = "";
11476
+ objectStatus.textContent = "";
11477
+ objectList.innerHTML = "";
11478
+ objectsByKey.clear();
11479
+ objectRowsByKey.clear();
11480
+ activeObjectRow = null;
11481
+ moreBtn.hidden = true;
11482
+ previewPane.textContent = "Select an object to preview.";
11483
+ }
11484
+ function getSelection() {
11485
+ return {
11486
+ bucket: currentBucket ?? undefined,
11487
+ ...currentMode === "prefix" ? { prefix: currentSearch || undefined } : { query: currentSearch || undefined },
11488
+ mode: currentMode,
11489
+ sort: currentSort,
11490
+ key: currentKey ?? undefined
11491
+ };
11492
+ }
11493
+ function dispose() {
11494
+ disposed = true;
11495
+ clear();
11496
+ }
11497
+ setMode("prefix");
11498
+ return { el: container, load, clear, dispose, getSelection };
11499
+ }
11500
+
10686
11501
  // web-src/views/database/schema-view.ts
10687
11502
  function createSchemaView() {
10688
11503
  const el = document.createElement("div");
@@ -11017,6 +11832,7 @@ ${frontmatter.yaml}
11017
11832
  });
11018
11833
  confirmBtn.addEventListener("click", async () => {
11019
11834
  const dbId = deps.getDbId();
11835
+ const schema = deps.getSchema();
11020
11836
  if (!dbId)
11021
11837
  return;
11022
11838
  const tables = getSelectedTables();
@@ -11027,11 +11843,12 @@ ${frontmatter.yaml}
11027
11843
  try {
11028
11844
  await postJson("/_db/snapshot/create", {
11029
11845
  db: dbId,
11846
+ ...schema ? { schema } : {},
11030
11847
  tables,
11031
11848
  note: noteInput.value.trim()
11032
11849
  });
11033
11850
  tableSelector.hidden = true;
11034
- scheduleAutoRefresh(dbId);
11851
+ scheduleAutoRefresh(dbId, schema);
11035
11852
  } catch {} finally {
11036
11853
  if (!disposed) {
11037
11854
  confirmBtn.disabled = false;
@@ -11039,11 +11856,12 @@ ${frontmatter.yaml}
11039
11856
  }
11040
11857
  }
11041
11858
  });
11042
- function scheduleAutoRefresh(dbId) {
11859
+ function scheduleAutoRefresh(dbId, schema) {
11043
11860
  const timer = setTimeout(() => {
11044
11861
  autoRefreshTimers.delete(timer);
11045
- if (disposed || deps.getDbId() !== dbId)
11862
+ if (disposed || deps.getDbId() !== dbId || deps.getSchema() !== schema) {
11046
11863
  return;
11864
+ }
11047
11865
  refreshAndAutoDiff();
11048
11866
  }, 3000);
11049
11867
  autoRefreshTimers.add(timer);
@@ -11051,17 +11869,22 @@ ${frontmatter.yaml}
11051
11869
  refreshBtn.addEventListener("click", refresh);
11052
11870
  async function refresh() {
11053
11871
  const dbId = deps.getDbId();
11872
+ const schema = deps.getSchema();
11054
11873
  if (!dbId)
11055
11874
  return;
11056
11875
  try {
11057
- const snapRes = await fetch(`/_db/snapshot/list?db=${encodeURIComponent(dbId)}`);
11876
+ const params = new URLSearchParams({ db: dbId });
11877
+ if (schema)
11878
+ params.set("schema", schema);
11879
+ const snapRes = await fetch(`/_db/snapshot/list?${params}`);
11058
11880
  if (snapRes.ok) {
11059
11881
  const data = await snapRes.json();
11060
- if (disposed || deps.getDbId() !== dbId)
11882
+ if (disposed || deps.getDbId() !== dbId || deps.getSchema() !== schema) {
11061
11883
  return;
11884
+ }
11062
11885
  snapshots = data.snapshots;
11063
11886
  }
11064
- if (disposed || deps.getDbId() !== dbId)
11887
+ if (disposed || deps.getDbId() !== dbId || deps.getSchema() !== schema)
11065
11888
  return;
11066
11889
  renderMain();
11067
11890
  } catch {}
@@ -11436,8 +12259,11 @@ ${frontmatter.yaml}
11436
12259
  try {
11437
12260
  const parsed = JSON.parse(data);
11438
12261
  const dbId = deps.getDbId();
12262
+ const schema = deps.getSchema();
11439
12263
  if (parsed.dbId && dbId && parsed.dbId !== dbId)
11440
12264
  return;
12265
+ if (parsed.schema && parsed.schema !== schema)
12266
+ return;
11441
12267
  if (parsed.action === "started" && parsed.id) {
11442
12268
  setActiveSnapshotId(parsed.id);
11443
12269
  }
@@ -11514,6 +12340,7 @@ ${frontmatter.yaml}
11514
12340
  let pageCache = new Map;
11515
12341
  let pendingPages = new Map;
11516
12342
  let loadGeneration = 0;
12343
+ let loadController = new AbortController;
11517
12344
  let rafId = 0;
11518
12345
  let statusEl = null;
11519
12346
  let filterTimer = null;
@@ -11614,10 +12441,18 @@ ${frontmatter.yaml}
11614
12441
  detailPanel.hidden = true;
11615
12442
  detailPanel.innerHTML = "";
11616
12443
  }
12444
+ function startNewLoadGeneration() {
12445
+ loadController.abort("db-grid:generation");
12446
+ loadController = new AbortController;
12447
+ loadGeneration++;
12448
+ }
12449
+ function isAbortError(err) {
12450
+ return err instanceof DOMException && err.name === "AbortError" || err instanceof Error && err.name === "AbortError";
12451
+ }
11617
12452
  function invalidateData() {
11618
12453
  pageCache = new Map;
11619
12454
  pendingPages = new Map;
11620
- loadGeneration++;
12455
+ startNewLoadGeneration();
11621
12456
  viewport.scrollTop = 0;
11622
12457
  resetSelectionAndDetail();
11623
12458
  return ensurePage(0);
@@ -11636,7 +12471,7 @@ ${frontmatter.yaml}
11636
12471
  filterRow.innerHTML = "";
11637
12472
  pageCache = new Map;
11638
12473
  pendingPages = new Map;
11639
- loadGeneration++;
12474
+ startNewLoadGeneration();
11640
12475
  cancelAnimationFrame(rafId);
11641
12476
  if (filterTimer)
11642
12477
  clearTimeout(filterTimer);
@@ -11849,6 +12684,7 @@ ${frontmatter.yaml}
11849
12684
  }
11850
12685
  pageCache = new Map;
11851
12686
  pendingPages = new Map;
12687
+ startNewLoadGeneration();
11852
12688
  resetSelectionAndDetail();
11853
12689
  renderHeader();
11854
12690
  renderViewport();
@@ -11860,8 +12696,9 @@ ${frontmatter.yaml}
11860
12696
  if (pending)
11861
12697
  return pending;
11862
12698
  const gen = loadGeneration;
12699
+ const signal = loadController.signal;
11863
12700
  const filters = collectFilters();
11864
- const promise = callbacks.fetchPage(currentTable, pageStart, PAGE_SIZE, sort, filters).then((data) => {
12701
+ const promise = callbacks.fetchPage(currentTable, pageStart, PAGE_SIZE, sort, filters, signal).then((data) => {
11865
12702
  if (gen !== loadGeneration)
11866
12703
  return;
11867
12704
  pageCache.set(pageStart, data.rows);
@@ -11870,6 +12707,8 @@ ${frontmatter.yaml}
11870
12707
  updateStatus();
11871
12708
  renderViewport();
11872
12709
  }).catch((err) => {
12710
+ if (gen !== loadGeneration || isAbortError(err))
12711
+ return;
11873
12712
  if (gen === loadGeneration) {
11874
12713
  showError(err instanceof Error ? err.message : String(err));
11875
12714
  }
@@ -12397,12 +13236,20 @@ ${frontmatter.yaml}
12397
13236
  function errorMessage(err) {
12398
13237
  return err instanceof Error ? err.message : String(err);
12399
13238
  }
13239
+ function isAbortError(err) {
13240
+ return err instanceof DOMException && err.name === "AbortError" || err instanceof Error && err.name === "AbortError";
13241
+ }
12400
13242
  function isSqlKind(kind) {
12401
13243
  return kind === "sqlite" || kind === "postgresql" || kind === "mysql";
12402
13244
  }
13245
+ function isPostgresKind(kind) {
13246
+ return kind === "postgresql";
13247
+ }
12403
13248
  function isSqlView(view) {
12404
13249
  return view === "data" || view === "query" || view === "schema" || view === "er" || view === "search" || view === "snapshot";
12405
13250
  }
13251
+ var HISTORY_SSE_REFRESH_DELAY_MS = 250;
13252
+ var TABLE_SELECT_FETCH_DELAY_MS = 50;
12406
13253
  function computeVisibility(kind, tab, userPrefersHistoryOpen) {
12407
13254
  const sqlMode = isSqlKind(kind);
12408
13255
  const tableScopedTab = tab === "data" || tab === "schema";
@@ -12421,7 +13268,8 @@ ${frontmatter.yaml}
12421
13268
  searchHidden: !sqlMode || tab !== "search",
12422
13269
  snapshotHidden: !sqlMode || tab !== "snapshot",
12423
13270
  redisHidden: kind !== "redis",
12424
- esHidden: kind !== "elasticsearch"
13271
+ esHidden: kind !== "elasticsearch",
13272
+ s3Hidden: kind !== "s3"
12425
13273
  };
12426
13274
  }
12427
13275
  function normalizeViewForDb(view, db) {
@@ -12453,20 +13301,27 @@ ${frontmatter.yaml}
12453
13301
  let currentDbInfo = null;
12454
13302
  let schemaCache = null;
12455
13303
  let lastFiles = [];
13304
+ let currentSchema = initial.schema ?? null;
12456
13305
  let currentTable = initial.table ?? null;
12457
13306
  let loadGeneration = 0;
13307
+ const tableSelectGuard = createAbortGuard();
13308
+ let historyRefreshPending = null;
12458
13309
  const dbSelect = document.createElement("select");
12459
13310
  dbSelect.className = "db-file-select";
12460
- dbSelect.title = "Select database file";
13311
+ dbSelect.title = "Select datastore";
13312
+ const schemaSelect = document.createElement("select");
13313
+ schemaSelect.className = "db-file-select db-schema-select";
13314
+ schemaSelect.title = "Select PostgreSQL schema";
13315
+ schemaSelect.hidden = true;
12461
13316
  const dbToolbar = document.createElement("div");
12462
13317
  dbToolbar.className = "db-toolbar";
12463
- dbToolbar.appendChild(dbSelect);
13318
+ dbToolbar.append(dbSelect, schemaSelect);
12464
13319
  const tabBar = document.createElement("div");
12465
13320
  tabBar.className = "db-tab-bar";
12466
13321
  const tabData = createInnerTab("Data", true);
12467
13322
  const tabSchema = createInnerTab("Schema", false);
12468
13323
  tabBar.append(tabData, tabSchema);
12469
- let currentTab = "data";
13324
+ let currentTab = initial.view ?? "data";
12470
13325
  const tableList = createTableList({
12471
13326
  onSelectTable: (table2) => selectTable(table2),
12472
13327
  onViewCreateTable: (table2) => showDdl(table2),
@@ -12480,7 +13335,7 @@ ${frontmatter.yaml}
12480
13335
  const toolsSection = document.createElement("div");
12481
13336
  toolsSection.className = "db-icon-toolbar";
12482
13337
  toolsSection.setAttribute("role", "toolbar");
12483
- toolsSection.setAttribute("aria-label", "Database tools");
13338
+ toolsSection.setAttribute("aria-label", "Datastore tools");
12484
13339
  const queryBtn = makeIconButton({
12485
13340
  label: "Query",
12486
13341
  title: "Query Editor",
@@ -12539,7 +13394,7 @@ ${frontmatter.yaml}
12539
13394
  window.addEventListener("mouseup", onUp);
12540
13395
  });
12541
13396
  const grid = createTableGrid({
12542
- fetchPage: (table2, offset, limit, sort, filters) => fetchTablePage(table2, offset, limit, sort, filters),
13397
+ fetchPage: (table2, offset, limit, sort, filters, signal) => fetchTablePage(table2, offset, limit, sort, filters, signal),
12543
13398
  getDbId: () => currentDbInfo?.id || null
12544
13399
  });
12545
13400
  const queryEditor = createQueryEditor({
@@ -12551,14 +13406,17 @@ ${frontmatter.yaml}
12551
13406
  const schemaView = createSchemaView();
12552
13407
  const erDiagram = createErDiagram();
12553
13408
  const globalSearchView = createGlobalSearchView({
12554
- getDbId: () => currentDbInfo?.id || null
13409
+ getDbId: () => currentDbInfo?.id || null,
13410
+ getSchema: () => currentSchema
12555
13411
  });
12556
13412
  const snapshotView = createSnapshotView({
12557
13413
  getDbId: () => currentDbInfo?.id || null,
13414
+ getSchema: () => currentSchema,
12558
13415
  getTables: () => schemaCache?.tables || []
12559
13416
  });
12560
13417
  const historyView = createQueryHistoryView({
12561
13418
  getDbId: () => currentDbInfo?.id || null,
13419
+ getSchema: () => currentSchema,
12562
13420
  copySqlToQuery: (sql) => {
12563
13421
  queryEditor.setSql(sql);
12564
13422
  setActiveTab("query");
@@ -12572,9 +13430,13 @@ ${frontmatter.yaml}
12572
13430
  onSelectionChange: () => cb.onStateChange()
12573
13431
  });
12574
13432
  esExplorer.el.hidden = true;
13433
+ const s3Explorer = createS3Explorer({
13434
+ onSelectionChange: () => cb.onStateChange()
13435
+ });
13436
+ s3Explorer.el.hidden = true;
12575
13437
  const mainContent = document.createElement("div");
12576
13438
  mainContent.className = "db-main-content";
12577
- mainContent.append(tabBar, grid.el, queryEditor.el, schemaView.el, erDiagram.el, globalSearchView.el, snapshotView.el, redisExplorer.el, esExplorer.el);
13439
+ mainContent.append(tabBar, grid.el, queryEditor.el, schemaView.el, erDiagram.el, globalSearchView.el, snapshotView.el, redisExplorer.el, esExplorer.el, s3Explorer.el);
12578
13440
  queryEditor.el.hidden = true;
12579
13441
  globalSearchView.el.hidden = true;
12580
13442
  snapshotView.el.hidden = true;
@@ -12636,6 +13498,7 @@ ${frontmatter.yaml}
12636
13498
  snapshotView.el.hidden = visibility.snapshotHidden;
12637
13499
  redisExplorer.el.hidden = visibility.redisHidden;
12638
13500
  esExplorer.el.hidden = visibility.esHidden;
13501
+ s3Explorer.el.hidden = visibility.s3Hidden;
12639
13502
  if (!sqlMode) {
12640
13503
  queryBtn.classList.remove("active");
12641
13504
  erBtn.classList.remove("active");
@@ -12643,7 +13506,7 @@ ${frontmatter.yaml}
12643
13506
  snapshotBtn.classList.remove("active");
12644
13507
  }
12645
13508
  historyToggle.classList.toggle("active", userPrefersHistoryOpen);
12646
- if (!visibility.historyPaneHidden)
13509
+ if (!visibility.historyPaneHidden && cb.isActive())
12647
13510
  historyView.refresh();
12648
13511
  }
12649
13512
  applyVisibility();
@@ -12680,6 +13543,33 @@ ${frontmatter.yaml}
12680
13543
  if (list2)
12681
13544
  setPaneStatus(list2, message, options);
12682
13545
  }
13546
+ function waitForTableSelectFetch(signal) {
13547
+ if (signal.aborted) {
13548
+ return Promise.reject(new DOMException("aborted", "AbortError"));
13549
+ }
13550
+ return new Promise((resolve, reject) => {
13551
+ let done = false;
13552
+ const cleanup = () => {
13553
+ signal.removeEventListener("abort", onAbort);
13554
+ };
13555
+ const timer = setTimeout(() => {
13556
+ if (done)
13557
+ return;
13558
+ done = true;
13559
+ cleanup();
13560
+ resolve();
13561
+ }, TABLE_SELECT_FETCH_DELAY_MS);
13562
+ const onAbort = () => {
13563
+ if (done)
13564
+ return;
13565
+ done = true;
13566
+ clearTimeout(timer);
13567
+ cleanup();
13568
+ reject(new DOMException("aborted", "AbortError"));
13569
+ };
13570
+ signal.addEventListener("abort", onAbort, { once: true });
13571
+ });
13572
+ }
12683
13573
  function setActiveTab(tab, updateUrl = true) {
12684
13574
  currentTab = normalizeViewForDb(tab, currentDbInfo);
12685
13575
  const tableScopedTab = currentTab === "data" || currentTab === "schema";
@@ -12700,6 +13590,7 @@ ${frontmatter.yaml}
12700
13590
  deps.setRoute({
12701
13591
  screen: "database",
12702
13592
  db: currentDbInfo?.id,
13593
+ schema: currentSchema ?? undefined,
12703
13594
  table: currentTable ?? undefined,
12704
13595
  tab: currentTab === "data" ? undefined : currentTab,
12705
13596
  range: deps.currentRange()
@@ -12714,20 +13605,54 @@ ${frontmatter.yaml}
12714
13605
  showSchema(active.dataset.table);
12715
13606
  });
12716
13607
  async function fetchDbFiles() {
13608
+ if (deps.fetchDbFiles)
13609
+ return deps.fetchDbFiles();
12717
13610
  const res = await deps.trackLoad(fetch("/_db/files"));
12718
13611
  if (!res.ok)
12719
13612
  return { files: [] };
12720
13613
  const data = await res.json();
12721
13614
  return data;
12722
13615
  }
13616
+ function withCurrentSchema(params) {
13617
+ if (currentSchema)
13618
+ params.set("schema", currentSchema);
13619
+ return params;
13620
+ }
13621
+ async function fetchSchemas(dbId, preferredSchema) {
13622
+ const params = new URLSearchParams({ db: dbId });
13623
+ if (preferredSchema)
13624
+ params.set("schema", preferredSchema);
13625
+ const res = await deps.trackLoad(fetch(`/_db/schemas?${params}`));
13626
+ if (!res.ok) {
13627
+ throw new Error(await responseErrorMessage(res, "failed to fetch schemas"));
13628
+ }
13629
+ return await res.json();
13630
+ }
13631
+ function renderSchemaOptions(schemas, selected) {
13632
+ schemaSelect.innerHTML = "";
13633
+ for (const schema of schemas) {
13634
+ const opt = document.createElement("option");
13635
+ opt.value = schema;
13636
+ opt.textContent = schema;
13637
+ schemaSelect.appendChild(opt);
13638
+ }
13639
+ schemaSelect.hidden = schemas.length === 0;
13640
+ schemaSelect.disabled = schemas.length === 0;
13641
+ if (selected && schemas.includes(selected)) {
13642
+ schemaSelect.value = selected;
13643
+ } else if (schemas.length > 0) {
13644
+ schemaSelect.value = schemas[0];
13645
+ }
13646
+ }
12723
13647
  async function fetchSchema(dbId) {
12724
- const res = await deps.trackLoad(fetch(`/_db/schema?db=${encodeURIComponent(dbId)}&includeColumns=1`));
13648
+ const params = withCurrentSchema(new URLSearchParams({ db: dbId, includeColumns: "1" }));
13649
+ const res = await deps.trackLoad(fetch(`/_db/schema?${params}`));
12725
13650
  if (!res.ok) {
12726
13651
  throw new Error(await responseErrorMessage(res, "failed to fetch schema"));
12727
13652
  }
12728
13653
  return await res.json();
12729
13654
  }
12730
- async function fetchTablePage(table2, offset, limit, sort, filters) {
13655
+ async function fetchTablePage(table2, offset, limit, sort, filters, signal) {
12731
13656
  if (!currentDbInfo)
12732
13657
  throw new Error("no database selected");
12733
13658
  const params = new URLSearchParams({
@@ -12736,6 +13661,7 @@ ${frontmatter.yaml}
12736
13661
  offset: String(offset),
12737
13662
  limit: String(limit)
12738
13663
  });
13664
+ withCurrentSchema(params);
12739
13665
  if (sort) {
12740
13666
  params.set("sort", sort.column);
12741
13667
  params.set("dir", sort.direction);
@@ -12743,7 +13669,7 @@ ${frontmatter.yaml}
12743
13669
  if (filters.length > 0) {
12744
13670
  params.set("filters", JSON.stringify(filters));
12745
13671
  }
12746
- const res = await fetch(`/_db/table?${params}`);
13672
+ const res = await fetch(`/_db/table?${params}`, signal ? { signal } : undefined);
12747
13673
  if (!res.ok) {
12748
13674
  throw new Error(await responseErrorMessage(res, "failed to fetch table"));
12749
13675
  }
@@ -12760,6 +13686,7 @@ ${frontmatter.yaml}
12760
13686
  },
12761
13687
  body: JSON.stringify({
12762
13688
  db: currentDbInfo.id,
13689
+ ...currentSchema ? { schema: currentSchema } : {},
12763
13690
  sql,
12764
13691
  saveHistory: true,
12765
13692
  source: "browser",
@@ -12770,15 +13697,16 @@ ${frontmatter.yaml}
12770
13697
  throw new Error(await responseErrorMessage(res, "failed to execute query"));
12771
13698
  }
12772
13699
  const result = await res.json();
12773
- if (userPrefersHistoryOpen)
12774
- historyView.refresh();
12775
13700
  return result;
12776
13701
  }
12777
- async function selectDb(dbId, explorerInitial, generation = loadGeneration, preferredTable) {
13702
+ async function selectDb(dbId, explorerInitial, generation = loadGeneration, preferredSchema, preferredTable, targetView) {
12778
13703
  if (generation !== loadGeneration || currentDbInfo?.id !== dbId)
12779
13704
  return;
13705
+ tableSelectGuard.dispose();
12780
13706
  currentTable = null;
12781
- if (currentDbInfo?.kind === "redis" || currentDbInfo?.kind === "elasticsearch") {
13707
+ if (currentDbInfo?.kind === "redis" || currentDbInfo?.kind === "elasticsearch" || currentDbInfo?.kind === "s3") {
13708
+ currentSchema = null;
13709
+ renderSchemaOptions([], null);
12782
13710
  currentTab = "data";
12783
13711
  tableList.render([]);
12784
13712
  grid.clear();
@@ -12788,10 +13716,16 @@ ${frontmatter.yaml}
12788
13716
  applyVisibility();
12789
13717
  if (currentDbInfo.kind === "redis") {
12790
13718
  esExplorer.clear();
13719
+ s3Explorer.clear();
12791
13720
  await redisExplorer.load(dbId, explorerInitial?.redis);
12792
- } else {
13721
+ } else if (currentDbInfo.kind === "elasticsearch") {
12793
13722
  redisExplorer.clear();
13723
+ s3Explorer.clear();
12794
13724
  await esExplorer.load(dbId, explorerInitial?.es);
13725
+ } else {
13726
+ redisExplorer.clear();
13727
+ esExplorer.clear();
13728
+ await s3Explorer.load(dbId, explorerInitial?.s3);
12795
13729
  }
12796
13730
  if (generation !== loadGeneration || currentDbInfo?.id !== dbId)
12797
13731
  return;
@@ -12800,10 +13734,39 @@ ${frontmatter.yaml}
12800
13734
  }
12801
13735
  redisExplorer.clear();
12802
13736
  esExplorer.clear();
13737
+ s3Explorer.clear();
12803
13738
  applyVisibility();
12804
13739
  tableList.render([]);
12805
13740
  setTableListStatus("Loading schema...");
12806
13741
  grid.clear();
13742
+ if (isPostgresKind(currentDbInfo?.kind)) {
13743
+ const desiredSchema = preferredSchema !== undefined ? preferredSchema : currentSchema;
13744
+ const schemas = await fetchSchemas(dbId, desiredSchema).catch((err) => {
13745
+ if (generation !== loadGeneration || currentDbInfo?.id !== dbId)
13746
+ return;
13747
+ const message = errorMessage(err);
13748
+ currentSchema = null;
13749
+ renderSchemaOptions([], null);
13750
+ schemaCache = null;
13751
+ tableList.render([]);
13752
+ setTableListStatus(message, { error: true });
13753
+ grid.showError(message);
13754
+ schemaView.clear();
13755
+ erDiagram.clear();
13756
+ cb.onStateChange();
13757
+ return null;
13758
+ });
13759
+ if (generation !== loadGeneration || currentDbInfo?.id !== dbId)
13760
+ return;
13761
+ if (!schemas)
13762
+ return;
13763
+ const schemaNames = schemas.schemas.map((s2) => s2.name);
13764
+ currentSchema = schemas.selectedSchema || desiredSchema || (schemaNames.includes("public") ? "public" : schemaNames[0]) || null;
13765
+ renderSchemaOptions(schemaNames, currentSchema);
13766
+ } else {
13767
+ currentSchema = null;
13768
+ renderSchemaOptions([], null);
13769
+ }
12807
13770
  const schema = await fetchSchema(dbId).catch((err) => {
12808
13771
  if (generation !== loadGeneration || currentDbInfo?.id !== dbId)
12809
13772
  return;
@@ -12822,15 +13785,19 @@ ${frontmatter.yaml}
12822
13785
  return;
12823
13786
  if (!schema)
12824
13787
  return;
13788
+ currentSchema = schema.schema || currentSchema;
12825
13789
  schemaCache = schema;
12826
13790
  tableList.render(schema.tables);
12827
13791
  grid.clear();
12828
13792
  schemaView.clear();
12829
13793
  erDiagram.clear();
12830
- setActiveTab("data", false);
13794
+ const normalizedTargetView = normalizeViewForDb(targetView, currentDbInfo);
13795
+ setActiveTab(normalizedTargetView === "schema" ? "schema" : "data", false);
12831
13796
  const initialTable = preferredTable || schema.tables[0]?.name;
12832
- if (initialTable) {
13797
+ if (initialTable && normalizedTargetView === "data") {
12833
13798
  await selectTable(initialTable, generation);
13799
+ } else if (initialTable && normalizedTargetView === "schema") {
13800
+ await selectTableSchemaOnly(initialTable, generation);
12834
13801
  }
12835
13802
  applyVisibility();
12836
13803
  cb.onStateChange();
@@ -12838,6 +13805,7 @@ ${frontmatter.yaml}
12838
13805
  async function selectTable(table2, generation = loadGeneration) {
12839
13806
  if (generation !== loadGeneration)
12840
13807
  return;
13808
+ const slot = tableSelectGuard.start();
12841
13809
  currentTable = table2;
12842
13810
  tableList.setActive(table2);
12843
13811
  if (!currentDbInfo)
@@ -12849,21 +13817,28 @@ ${frontmatter.yaml}
12849
13817
  deps.setRoute({
12850
13818
  screen: "database",
12851
13819
  db: currentDbInfo.id,
13820
+ schema: currentSchema ?? undefined,
12852
13821
  table: table2,
12853
13822
  tab: currentTab === "data" ? undefined : currentTab,
12854
13823
  range: deps.currentRange()
12855
13824
  }, true);
12856
13825
  try {
12857
- const data = await deps.trackLoad(fetchTablePage(table2, 0, 200, null, []));
12858
- if (generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
13826
+ await waitForTableSelectFetch(slot.signal);
13827
+ if (slot.isStale() || generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
13828
+ return;
13829
+ }
13830
+ const data = await deps.trackLoad(fetchTablePage(table2, 0, 200, null, [], slot.signal));
13831
+ if (slot.isStale() || generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
12859
13832
  return;
12860
13833
  }
12861
13834
  grid.load(table2, data);
12862
13835
  } catch (err) {
12863
- if (generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
13836
+ if (slot.isStale() || isAbortError(err) || generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
12864
13837
  return;
12865
13838
  }
12866
13839
  grid.showError(errorMessage(err));
13840
+ } finally {
13841
+ slot.finish();
12867
13842
  }
12868
13843
  if (currentTab === "schema") {
12869
13844
  const columns = await fetchColumns(table2);
@@ -12873,13 +13848,28 @@ ${frontmatter.yaml}
12873
13848
  schemaView.render(table2, columns, schemaCache?.indexes || []);
12874
13849
  }
12875
13850
  }
13851
+ async function selectTableSchemaOnly(table2, generation = loadGeneration) {
13852
+ if (generation !== loadGeneration)
13853
+ return;
13854
+ currentTable = table2;
13855
+ tableList.setActive(table2);
13856
+ if (!currentDbInfo)
13857
+ return;
13858
+ const requestDbId = currentDbInfo.id;
13859
+ setActiveTab("schema", false);
13860
+ const columns = await fetchColumns(table2);
13861
+ if (generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
13862
+ return;
13863
+ }
13864
+ schemaView.render(table2, columns, schemaCache?.indexes || []);
13865
+ }
12876
13866
  async function fetchColumns(table2) {
12877
13867
  if (schemaCache?.columnsMap?.[table2]) {
12878
13868
  return schemaCache.columnsMap[table2];
12879
13869
  }
12880
13870
  if (!currentDbInfo)
12881
13871
  return [];
12882
- const res = await fetch(`/_db/columns?db=${encodeURIComponent(currentDbInfo.id)}&table=${encodeURIComponent(table2)}`);
13872
+ const res = await fetch(`/_db/columns?${withCurrentSchema(new URLSearchParams({ db: currentDbInfo.id, table: table2 }))}`);
12883
13873
  if (!res.ok)
12884
13874
  return [];
12885
13875
  const data = await res.json();
@@ -12897,7 +13887,7 @@ ${frontmatter.yaml}
12897
13887
  return;
12898
13888
  setActiveTab("schema");
12899
13889
  try {
12900
- const res = await fetch(`/_db/ddl?db=${encodeURIComponent(currentDbInfo.id)}&table=${encodeURIComponent(table2)}`);
13890
+ const res = await fetch(`/_db/ddl?${withCurrentSchema(new URLSearchParams({ db: currentDbInfo.id, table: table2 }))}`);
12901
13891
  if (!res.ok)
12902
13892
  return;
12903
13893
  const data = await res.json();
@@ -12930,6 +13920,33 @@ ${frontmatter.yaml}
12930
13920
  dbSelect.addEventListener("change", () => {
12931
13921
  handleDbSelectChange();
12932
13922
  });
13923
+ schemaSelect.addEventListener("change", () => {
13924
+ handleSchemaSelectChange();
13925
+ });
13926
+ async function handleSchemaSelectChange() {
13927
+ if (!currentDbInfo || !isPostgresKind(currentDbInfo.kind))
13928
+ return;
13929
+ const schema = schemaSelect.value || null;
13930
+ if (!schema || schema === currentSchema)
13931
+ return;
13932
+ const dbId = currentDbInfo.id;
13933
+ currentSchema = schema;
13934
+ const generation = ++loadGeneration;
13935
+ clearDockerNotice();
13936
+ await selectDb(dbId, undefined, generation, schema, null);
13937
+ if (generation !== loadGeneration || currentDbInfo?.id !== dbId || currentSchema !== schema) {
13938
+ return;
13939
+ }
13940
+ deps.setRoute({
13941
+ screen: "database",
13942
+ db: dbId,
13943
+ schema,
13944
+ table: currentTable ?? undefined,
13945
+ tab: currentTab === "data" ? undefined : currentTab,
13946
+ range: deps.currentRange()
13947
+ }, true);
13948
+ cb.onStateChange();
13949
+ }
12933
13950
  async function handleDbSelectChange() {
12934
13951
  const dbId = dbSelect.value;
12935
13952
  if (!dbId)
@@ -12944,13 +13961,15 @@ ${frontmatter.yaml}
12944
13961
  sizeBytes: file?.sizeBytes || 0,
12945
13962
  kind: file?.kind || "sqlite"
12946
13963
  };
13964
+ currentSchema = null;
12947
13965
  clearDockerNotice();
12948
- await selectDb(dbId, undefined, generation);
13966
+ await selectDb(dbId, undefined, generation, null);
12949
13967
  if (generation !== loadGeneration || currentDbInfo?.id !== dbId)
12950
13968
  return;
12951
13969
  deps.setRoute({
12952
13970
  screen: "database",
12953
13971
  db: dbId,
13972
+ schema: currentSchema ?? undefined,
12954
13973
  table: currentTable ?? undefined,
12955
13974
  tab: currentTab === "data" ? undefined : currentTab,
12956
13975
  range: deps.currentRange()
@@ -12959,7 +13978,8 @@ ${frontmatter.yaml}
12959
13978
  }
12960
13979
  let pendingRedisInitial = initial.redis;
12961
13980
  let pendingEsInitial = initial.es;
12962
- async function enter(db, table2, view, options = {}) {
13981
+ let pendingS3Initial = initial.s3;
13982
+ async function enter(db, schema, table2, view, options = {}) {
12963
13983
  const generation = ++loadGeneration;
12964
13984
  const filesResponse = await fetchDbFiles();
12965
13985
  if (generation !== loadGeneration)
@@ -12975,21 +13995,23 @@ ${frontmatter.yaml}
12975
13995
  if (files.length === 0) {
12976
13996
  const opt = document.createElement("option");
12977
13997
  opt.value = "";
12978
- opt.textContent = "No database files found";
13998
+ opt.textContent = "No datastores found";
12979
13999
  dbSelect.appendChild(opt);
12980
14000
  dbSelect.disabled = true;
12981
14001
  cb.onStateChange();
12982
14002
  return;
12983
14003
  }
12984
14004
  dbSelect.disabled = false;
14005
+ const optionsFragment = document.createDocumentFragment();
12985
14006
  for (const f2 of files) {
12986
14007
  const opt = document.createElement("option");
12987
14008
  opt.value = f2.id;
12988
14009
  const isDocker = f2.id.startsWith("docker:");
12989
14010
  const label = isDocker ? `${f2.name} (Docker)` : `${f2.path} (${formatSize(f2.sizeBytes)})`;
12990
14011
  opt.textContent = label;
12991
- dbSelect.appendChild(opt);
14012
+ optionsFragment.appendChild(opt);
12992
14013
  }
14014
+ dbSelect.appendChild(optionsFragment);
12993
14015
  const autoSelectFirst = options.autoSelectFirst ?? true;
12994
14016
  if (db && !files.find((f2) => f2.id === db)) {
12995
14017
  db = autoSelectFirst ? files[0].id : null;
@@ -12997,14 +14019,17 @@ ${frontmatter.yaml}
12997
14019
  if (!db && !autoSelectFirst) {
12998
14020
  dbSelect.value = "";
12999
14021
  currentDbInfo = null;
14022
+ currentSchema = null;
13000
14023
  currentTable = null;
13001
14024
  schemaCache = null;
14025
+ renderSchemaOptions([], null);
13002
14026
  tableList.render([]);
13003
14027
  grid.clear();
13004
14028
  schemaView.clear();
13005
14029
  erDiagram.clear();
13006
14030
  redisExplorer.clear();
13007
14031
  esExplorer.clear();
14032
+ s3Explorer.clear();
13008
14033
  setActiveTab("data", false);
13009
14034
  cb.onStateChange();
13010
14035
  return;
@@ -13014,14 +14039,16 @@ ${frontmatter.yaml}
13014
14039
  currentDbInfo = files.find((f2) => f2.id === target) || null;
13015
14040
  const explorerInitial = {
13016
14041
  redis: pendingRedisInitial,
13017
- es: pendingEsInitial
14042
+ es: pendingEsInitial,
14043
+ s3: pendingS3Initial
13018
14044
  };
13019
14045
  pendingRedisInitial = undefined;
13020
14046
  pendingEsInitial = undefined;
13021
- await selectDb(target, explorerInitial, generation, table2);
14047
+ pendingS3Initial = undefined;
14048
+ await selectDb(target, explorerInitial, generation, schema !== undefined ? schema : currentSchema, table2, view);
13022
14049
  if (generation !== loadGeneration || currentDbInfo?.id !== target)
13023
14050
  return;
13024
- if (currentDbInfo?.kind === "redis" || currentDbInfo?.kind === "elasticsearch") {
14051
+ if (currentDbInfo?.kind === "redis" || currentDbInfo?.kind === "elasticsearch" || currentDbInfo?.kind === "s3") {
13025
14052
  cb.onStateChange();
13026
14053
  return;
13027
14054
  }
@@ -13051,6 +14078,8 @@ ${frontmatter.yaml}
13051
14078
  function applyAnnotationTarget(target) {
13052
14079
  if (!target || target.db !== currentDbInfo?.id)
13053
14080
  return;
14081
+ if (target.schema && target.schema !== currentSchema)
14082
+ return;
13054
14083
  if (target.data) {
13055
14084
  if (target.table && target.table !== currentTable)
13056
14085
  return;
@@ -13076,19 +14105,28 @@ ${frontmatter.yaml}
13076
14105
  }
13077
14106
  }
13078
14107
  function handleSse(event, data) {
13079
- if (userPrefersHistoryOpen)
13080
- historyView.refresh();
14108
+ if (userPrefersHistoryOpen && cb.isActive()) {
14109
+ if (historyRefreshPending !== null)
14110
+ clearTimeout(historyRefreshPending);
14111
+ historyRefreshPending = setTimeout(() => {
14112
+ historyRefreshPending = null;
14113
+ if (userPrefersHistoryOpen && cb.isActive()) {
14114
+ historyView.refresh({ force: true });
14115
+ }
14116
+ }, HISTORY_SSE_REFRESH_DELAY_MS);
14117
+ }
13081
14118
  if (event === "db-snapshot" && data) {
13082
14119
  snapshotView.handleSse(data);
13083
14120
  }
13084
14121
  }
13085
14122
  function getState() {
13086
- const activeTable = tableList.el.querySelector(".db-table-item.active");
14123
+ const loaded = !!currentDbInfo;
13087
14124
  const state = {
13088
14125
  id: cb.tabId,
13089
- dbId: currentDbInfo?.id ?? null,
13090
- table: currentTable ?? activeTable?.dataset.table ?? null,
13091
- view: currentTab
14126
+ dbId: currentDbInfo?.id ?? initial.dbId ?? null,
14127
+ schema: loaded ? currentSchema : initial.schema ?? currentSchema,
14128
+ table: loaded ? currentTable : initial.table ?? currentTable ?? null,
14129
+ view: loaded ? currentTab : initial.view ?? currentTab
13092
14130
  };
13093
14131
  const sqlDraft = queryEditor.getSql();
13094
14132
  if (sqlDraft)
@@ -13099,7 +14137,14 @@ ${frontmatter.yaml}
13099
14137
  state.historyHeight = historyPane.style.height;
13100
14138
  if (sidebar.style.width)
13101
14139
  state.sidebarWidth = sidebar.style.width;
13102
- if (currentDbInfo?.kind === "redis") {
14140
+ if (!loaded) {
14141
+ if (initial.redis)
14142
+ state.redis = initial.redis;
14143
+ if (initial.es)
14144
+ state.es = initial.es;
14145
+ if (initial.s3)
14146
+ state.s3 = initial.s3;
14147
+ } else if (currentDbInfo?.kind === "redis") {
13103
14148
  const sel = redisExplorer.getSelection();
13104
14149
  if (sel.dbIndex !== undefined || sel.key !== undefined || sel.keyFilter !== undefined) {
13105
14150
  state.redis = sel;
@@ -13109,6 +14154,11 @@ ${frontmatter.yaml}
13109
14154
  if (sel.index !== undefined || sel.query !== undefined) {
13110
14155
  state.es = sel;
13111
14156
  }
14157
+ } else if (currentDbInfo?.kind === "s3") {
14158
+ const sel = s3Explorer.getSelection();
14159
+ if (sel.bucket !== undefined || sel.prefix !== undefined || sel.query !== undefined || sel.key !== undefined || sel.mode !== "prefix" || sel.sort !== "updated-desc") {
14160
+ state.s3 = sel;
14161
+ }
13112
14162
  }
13113
14163
  return state;
13114
14164
  }
@@ -13118,6 +14168,7 @@ ${frontmatter.yaml}
13118
14168
  const target = {
13119
14169
  kind: "database",
13120
14170
  db: currentDbInfo.id,
14171
+ ...currentSchema ? { schema: currentSchema } : {},
13121
14172
  ...currentTable ? { table: currentTable } : {},
13122
14173
  tab: currentTab
13123
14174
  };
@@ -13134,15 +14185,22 @@ ${frontmatter.yaml}
13134
14185
  function getLabel() {
13135
14186
  if (!currentDbInfo)
13136
14187
  return labelFromDbId(initial.dbId);
14188
+ const suffix = currentSchema ? ` / ${currentSchema}` : "";
13137
14189
  if (currentDbInfo.id.startsWith("docker:")) {
13138
14190
  const m = currentDbInfo.name.match(/^(\S+)/);
13139
- return m ? m[1] : currentDbInfo.name;
14191
+ return `${m ? m[1] : currentDbInfo.name}${suffix}`;
13140
14192
  }
13141
14193
  const lastSlash = currentDbInfo.path.lastIndexOf("/");
13142
- return lastSlash >= 0 ? currentDbInfo.path.slice(lastSlash + 1) : currentDbInfo.path;
14194
+ const label = lastSlash >= 0 ? currentDbInfo.path.slice(lastSlash + 1) : currentDbInfo.path;
14195
+ return `${label}${suffix}`;
13143
14196
  }
13144
14197
  function dispose() {
13145
14198
  loadGeneration++;
14199
+ tableSelectGuard.dispose();
14200
+ if (historyRefreshPending !== null) {
14201
+ clearTimeout(historyRefreshPending);
14202
+ historyRefreshPending = null;
14203
+ }
13146
14204
  grid.destroy();
13147
14205
  schemaView.clear();
13148
14206
  erDiagram.dispose();
@@ -13153,7 +14211,9 @@ ${frontmatter.yaml}
13153
14211
  historyView.clear();
13154
14212
  redisExplorer.dispose();
13155
14213
  esExplorer.dispose();
14214
+ s3Explorer.dispose();
13156
14215
  currentDbInfo = null;
14216
+ currentSchema = null;
13157
14217
  currentTable = null;
13158
14218
  schemaCache = null;
13159
14219
  }
@@ -13195,6 +14255,7 @@ ${frontmatter.yaml}
13195
14255
  let mounted = false;
13196
14256
  const tabsById = new Map;
13197
14257
  const paneReadyById = new Map;
14258
+ const lazyInitialById = new Map;
13198
14259
  let activeTabId = null;
13199
14260
  let draggingTabId = null;
13200
14261
  let dropTargetId = null;
@@ -13202,9 +14263,40 @@ ${frontmatter.yaml}
13202
14263
  let restoring = false;
13203
14264
  let savePending = null;
13204
14265
  let saveChain = Promise.resolve();
14266
+ let lastSavedTabsRaw = null;
14267
+ let pendingSavedTabsRaw = null;
14268
+ let saveController = null;
13205
14269
  let lifecycleSeq = 0;
13206
14270
  let enterQueue = Promise.resolve();
13207
14271
  let unloadListenerInstalled = false;
14272
+ let dbFilesCache = null;
14273
+ function fetchDbFilesCached() {
14274
+ const now = Date.now();
14275
+ if (dbFilesCache?.value && dbFilesCache.expiresAt > now) {
14276
+ return Promise.resolve(dbFilesCache.value);
14277
+ }
14278
+ if (dbFilesCache?.promise)
14279
+ return dbFilesCache.promise;
14280
+ const promise = deps.trackLoad(fetch("/_db/files")).then(async (res) => {
14281
+ if (!res.ok) {
14282
+ const value2 = { files: [] };
14283
+ dbFilesCache = { value: value2, expiresAt: Date.now() + 1e4 };
14284
+ return value2;
14285
+ }
14286
+ const value = await res.json();
14287
+ dbFilesCache = { value, expiresAt: Date.now() + 1e4 };
14288
+ return value;
14289
+ }).catch(() => {
14290
+ const value = { files: [] };
14291
+ dbFilesCache = { value, expiresAt: Date.now() + 1e4 };
14292
+ return value;
14293
+ }).finally(() => {
14294
+ if (dbFilesCache?.promise === promise)
14295
+ dbFilesCache = null;
14296
+ });
14297
+ dbFilesCache = { promise };
14298
+ return promise;
14299
+ }
13208
14300
  function scheduleSave() {
13209
14301
  if (!mounted || restoring)
13210
14302
  return;
@@ -13212,6 +14304,10 @@ ${frontmatter.yaml}
13212
14304
  clearTimeout(savePending);
13213
14305
  savePending = setTimeout(saveNow, 500);
13214
14306
  }
14307
+ function abortActiveSave() {
14308
+ saveController?.abort();
14309
+ saveController = null;
14310
+ }
13215
14311
  async function saveNow(options = {}) {
13216
14312
  savePending = null;
13217
14313
  if (!mounted || restoring)
@@ -13224,7 +14320,33 @@ ${frontmatter.yaml}
13224
14320
  return;
13225
14321
  const body = { version: 1, tabs, activeTabId };
13226
14322
  const raw = JSON.stringify(body);
14323
+ if (raw === lastSavedTabsRaw)
14324
+ return;
14325
+ if (!options.keepalive && raw === pendingSavedTabsRaw)
14326
+ return;
14327
+ pendingSavedTabsRaw = raw;
14328
+ abortActiveSave();
14329
+ if (options.keepalive) {
14330
+ try {
14331
+ await fetch("/_db/tabs", {
14332
+ method: "PUT",
14333
+ headers: {
14334
+ "Content-Type": "application/json",
14335
+ "X-Code-Viewer-Action": "1"
14336
+ },
14337
+ body: raw,
14338
+ keepalive: true
14339
+ });
14340
+ lastSavedTabsRaw = raw;
14341
+ } catch {} finally {
14342
+ if (pendingSavedTabsRaw === raw)
14343
+ pendingSavedTabsRaw = null;
14344
+ }
14345
+ return;
14346
+ }
13227
14347
  saveChain = saveChain.catch(() => {}).then(async () => {
14348
+ const controller = new AbortController;
14349
+ saveController = controller;
13228
14350
  try {
13229
14351
  await fetch("/_db/tabs", {
13230
14352
  method: "PUT",
@@ -13233,9 +14355,17 @@ ${frontmatter.yaml}
13233
14355
  "X-Code-Viewer-Action": "1"
13234
14356
  },
13235
14357
  body: raw,
13236
- keepalive: options.keepalive
14358
+ signal: controller.signal
13237
14359
  });
13238
- } catch {}
14360
+ lastSavedTabsRaw = raw;
14361
+ } catch (err) {
14362
+ if (!isAbortError(err)) {}
14363
+ } finally {
14364
+ if (saveController === controller)
14365
+ saveController = null;
14366
+ if (pendingSavedTabsRaw === raw)
14367
+ pendingSavedTabsRaw = null;
14368
+ }
13239
14369
  });
13240
14370
  await saveChain;
13241
14371
  }
@@ -13288,6 +14418,9 @@ ${frontmatter.yaml}
13288
14418
  if (!restoring)
13289
14419
  syncActiveRoute();
13290
14420
  scheduleSave();
14421
+ if (mounted && !restoring) {
14422
+ ensureInitialEnter(id)?.catch(() => {});
14423
+ }
13291
14424
  }
13292
14425
  function syncActiveRoute() {
13293
14426
  if (!activeTabId)
@@ -13299,6 +14432,7 @@ ${frontmatter.yaml}
13299
14432
  deps.setRoute({
13300
14433
  screen: "database",
13301
14434
  db: s2.dbId ?? undefined,
14435
+ schema: s2.schema ?? undefined,
13302
14436
  table: s2.table ?? undefined,
13303
14437
  tab: s2.view === "data" ? undefined : s2.view,
13304
14438
  range: deps.currentRange()
@@ -13319,10 +14453,12 @@ ${frontmatter.yaml}
13319
14453
  for (const tab of tabs) {
13320
14454
  const key = JSON.stringify({
13321
14455
  dbId: tab.dbId,
14456
+ schema: tab.schema ?? null,
13322
14457
  table: tab.table,
13323
14458
  view: tab.view,
13324
14459
  redis: tab.redis ?? null,
13325
- es: tab.es ?? null
14460
+ es: tab.es ?? null,
14461
+ s3: tab.s3 ?? null
13326
14462
  });
13327
14463
  if (seen.has(key))
13328
14464
  continue;
@@ -13537,7 +14673,7 @@ ${frontmatter.yaml}
13537
14673
  closeTab(id);
13538
14674
  }
13539
14675
  });
13540
- const pane = createTabPane(deps, {
14676
+ const pane = createTabPane({ ...deps, fetchDbFiles: fetchDbFilesCached }, {
13541
14677
  tabId: id,
13542
14678
  isActive: () => activeTabId === id,
13543
14679
  canSyncRoute: () => !restoring,
@@ -13549,6 +14685,7 @@ ${frontmatter.yaml}
13549
14685
  }
13550
14686
  }, {
13551
14687
  dbId: initial?.dbId ?? null,
14688
+ schema: initial?.schema ?? null,
13552
14689
  table: initial?.table ?? null,
13553
14690
  view: initial?.view ?? undefined,
13554
14691
  sqlDraft: initial?.sqlDraft,
@@ -13556,13 +14693,15 @@ ${frontmatter.yaml}
13556
14693
  historyHeight: initial?.historyHeight,
13557
14694
  sidebarWidth: initial?.sidebarWidth,
13558
14695
  redis: initial?.redis,
13559
- es: initial?.es
14696
+ es: initial?.es,
14697
+ s3: initial?.s3
13560
14698
  });
13561
14699
  pane.el.hidden = true;
13562
14700
  tabsList.appendChild(chip);
13563
14701
  tabHost.appendChild(pane.el);
13564
14702
  tabsById.set(id, { pane, chip, label: labelEl, closeBtn });
13565
- setActive(id);
14703
+ if (options.activate !== false)
14704
+ setActive(id);
13566
14705
  if (!options.deferInitialEnter) {
13567
14706
  startInitialEnter(id, initial, options);
13568
14707
  }
@@ -13577,7 +14716,7 @@ ${frontmatter.yaml}
13577
14716
  if (options.annotationTarget)
13578
14717
  restoring = true;
13579
14718
  try {
13580
- await pane.enter(initial?.dbId || undefined, initial?.table || undefined, initial?.view || undefined, options);
14719
+ await pane.enter(initial?.dbId || undefined, initial?.schema || undefined, initial?.table || undefined, initial?.view || undefined, options);
13581
14720
  refreshChipLabel(id);
13582
14721
  } finally {
13583
14722
  if (options.annotationTarget)
@@ -13588,6 +14727,16 @@ ${frontmatter.yaml}
13588
14727
  paneReadyById.set(id, ready);
13589
14728
  return ready;
13590
14729
  }
14730
+ function ensureInitialEnter(id) {
14731
+ const ready = paneReadyById.get(id);
14732
+ if (ready)
14733
+ return ready;
14734
+ if (!lazyInitialById.has(id))
14735
+ return null;
14736
+ const initial = lazyInitialById.get(id);
14737
+ lazyInitialById.delete(id);
14738
+ return startInitialEnter(id, initial, { autoSelectFirst: false });
14739
+ }
13591
14740
  function closeTab(id) {
13592
14741
  const entry = tabsById.get(id);
13593
14742
  if (!entry)
@@ -13598,6 +14747,7 @@ ${frontmatter.yaml}
13598
14747
  entry.chip.remove();
13599
14748
  tabsById.delete(id);
13600
14749
  paneReadyById.delete(id);
14750
+ lazyInitialById.delete(id);
13601
14751
  closeDbIfUnused(closedDbId);
13602
14752
  if (activeTabId !== id) {
13603
14753
  scheduleSave();
@@ -13614,35 +14764,46 @@ ${frontmatter.yaml}
13614
14764
  newTabBtn.addEventListener("click", () => {
13615
14765
  openTab({ dbId: null, table: null, view: "data" }, { autoSelectFirst: false });
13616
14766
  });
13617
- function routeMatchesState(state, db, table2, view) {
13618
- if (!db && (table2 || view) && !state.dbId)
14767
+ function routeMatchesState(state, db, schema, table2, view) {
14768
+ if (!db && (schema || table2 || view) && !state.dbId)
13619
14769
  return false;
13620
14770
  if (db !== undefined && state.dbId !== db)
13621
14771
  return false;
14772
+ if (schema !== undefined) {
14773
+ const routeSchema = schema || "public";
14774
+ const stateSchema = state.schema || "public";
14775
+ if (stateSchema !== routeSchema)
14776
+ return false;
14777
+ }
13622
14778
  if (table2 !== undefined && state.table !== table2)
13623
14779
  return false;
13624
14780
  if (view !== undefined && state.view !== view)
13625
14781
  return false;
13626
14782
  return true;
13627
14783
  }
13628
- async function applyRouteToTab(db, table2, view, options = {}) {
13629
- async function enterPane(pane, targetDb, targetTable, targetView, enterOptions) {
14784
+ async function applyRouteToTab(db, schema, table2, view, options = {}) {
14785
+ async function enterPane(pane, targetDb, targetSchema, targetTable, targetView, enterOptions) {
13630
14786
  if (!enterOptions?.annotationTarget) {
13631
- await pane.enter(targetDb, targetTable, targetView, enterOptions);
14787
+ await pane.enter(targetDb, targetSchema, targetTable, targetView, enterOptions);
13632
14788
  return;
13633
14789
  }
13634
14790
  restoring = true;
13635
14791
  try {
13636
- await pane.enter(targetDb, targetTable, targetView, enterOptions);
14792
+ await pane.enter(targetDb, targetSchema, targetTable, targetView, enterOptions);
13637
14793
  } finally {
13638
14794
  restoring = false;
13639
14795
  }
13640
14796
  }
13641
14797
  for (const [id2, entry] of tabsById) {
13642
- if (routeMatchesState(entry.pane.getState(), db, table2, view)) {
14798
+ if (routeMatchesState(entry.pane.getState(), db, schema, table2, view)) {
13643
14799
  setActive(id2);
14800
+ const ready2 = ensureInitialEnter(id2);
14801
+ if (ready2)
14802
+ await ready2;
14803
+ if (!mounted)
14804
+ return;
13644
14805
  if (options.annotationTarget) {
13645
- await enterPane(entry.pane, db, table2, view, options);
14806
+ await enterPane(entry.pane, db, schema, table2, view, options);
13646
14807
  if (!mounted)
13647
14808
  return;
13648
14809
  }
@@ -13654,7 +14815,7 @@ ${frontmatter.yaml}
13654
14815
  const targetId = activeTabId;
13655
14816
  const active = tabsById.get(activeTabId);
13656
14817
  if (active) {
13657
- await enterPane(active.pane, db, table2, view, options);
14818
+ await enterPane(active.pane, db, schema, table2, view, options);
13658
14819
  if (!mounted)
13659
14820
  return;
13660
14821
  refreshChipLabel(targetId);
@@ -13663,6 +14824,7 @@ ${frontmatter.yaml}
13663
14824
  }
13664
14825
  const id = openTab({
13665
14826
  dbId: db ?? null,
14827
+ schema: schema ?? null,
13666
14828
  table: table2 ?? null,
13667
14829
  view: view ?? "data"
13668
14830
  }, { autoSelectFirst: db === undefined, ...options });
@@ -13681,7 +14843,17 @@ ${frontmatter.yaml}
13681
14843
  return null;
13682
14844
  }
13683
14845
  }
13684
- async function doEnter(seq, db, table2, view, options = {}) {
14846
+ function parseSseSchema(data) {
14847
+ if (!data)
14848
+ return null;
14849
+ try {
14850
+ const parsed = JSON.parse(data);
14851
+ return typeof parsed.schema === "string" ? parsed.schema : null;
14852
+ } catch {
14853
+ return null;
14854
+ }
14855
+ }
14856
+ async function doEnter(seq, db, schema, table2, view, options = {}) {
13685
14857
  if (seq !== lifecycleSeq)
13686
14858
  return;
13687
14859
  const firstMount = !mounted;
@@ -13710,8 +14882,6 @@ ${frontmatter.yaml}
13710
14882
  return;
13711
14883
  if (restored && restored.tabs.length > 0) {
13712
14884
  restoring = true;
13713
- const restoredIds = [];
13714
- const restoredById = new Map;
13715
14885
  try {
13716
14886
  const restoredTabs = dedupeTabs(restored.tabs);
13717
14887
  for (const t2 of restoredTabs) {
@@ -13719,33 +14889,26 @@ ${frontmatter.yaml}
13719
14889
  return;
13720
14890
  const id = openTab(t2, {
13721
14891
  autoSelectFirst: false,
13722
- deferInitialEnter: true
14892
+ deferInitialEnter: true,
14893
+ activate: false
13723
14894
  });
13724
- restoredIds.push(id);
13725
- restoredById.set(id, t2);
14895
+ lazyInitialById.set(id, t2);
13726
14896
  }
13727
14897
  const targetId = restored.activeTabId && tabsById.has(restored.activeTabId) ? restored.activeTabId : tabsById.keys().next().value;
13728
14898
  if (targetId)
13729
14899
  setActive(targetId);
13730
14900
  if (targetId) {
13731
- await startInitialEnter(targetId, restoredById.get(targetId), {
13732
- autoSelectFirst: false
13733
- });
14901
+ const ready = ensureInitialEnter(targetId);
14902
+ if (ready)
14903
+ await ready;
13734
14904
  }
13735
14905
  if (!mounted || seq !== lifecycleSeq)
13736
14906
  return;
13737
- for (const id of restoredIds) {
13738
- if (id === targetId)
13739
- continue;
13740
- startInitialEnter(id, restoredById.get(id), {
13741
- autoSelectFirst: false
13742
- }).catch(() => {});
13743
- }
13744
14907
  } finally {
13745
14908
  restoring = false;
13746
14909
  }
13747
- if (db || table2 || view) {
13748
- await applyRouteToTab(db, table2, view, {
14910
+ if (db || schema || table2 || view) {
14911
+ await applyRouteToTab(db, schema ?? null, table2, view, {
13749
14912
  ...options,
13750
14913
  reuseActiveTab: options.reuseActiveTab ?? true
13751
14914
  });
@@ -13770,6 +14933,7 @@ ${frontmatter.yaml}
13770
14933
  if (tabsById.size === 0) {
13771
14934
  const id = openTab({
13772
14935
  dbId: db || null,
14936
+ schema: schema || null,
13773
14937
  table: table2 || null,
13774
14938
  view: view || "data"
13775
14939
  }, { autoSelectFirst: !db, ...options });
@@ -13785,8 +14949,8 @@ ${frontmatter.yaml}
13785
14949
  const active = tabsById.get(activeTabId);
13786
14950
  if (!active)
13787
14951
  return;
13788
- if (db || table2 || view) {
13789
- await applyRouteToTab(db, table2, view, {
14952
+ if (db || schema || table2 || view) {
14953
+ await applyRouteToTab(db, schema ?? null, table2, view, {
13790
14954
  ...options,
13791
14955
  reuseActiveTab: options.reuseActiveTab ?? true
13792
14956
  });
@@ -13794,9 +14958,9 @@ ${frontmatter.yaml}
13794
14958
  syncActiveRoute();
13795
14959
  }
13796
14960
  }
13797
- async function enter(db, table2, view, options = {}) {
14961
+ async function enter(db, schema, table2, view, options = {}) {
13798
14962
  const seq = lifecycleSeq;
13799
- enterQueue = enterQueue.catch(() => {}).then(() => doEnter(seq, db, table2, view, options));
14963
+ enterQueue = enterQueue.catch(() => {}).then(() => doEnter(seq, db, schema, table2, view, options));
13800
14964
  await enterQueue;
13801
14965
  }
13802
14966
  function leave() {
@@ -13808,6 +14972,7 @@ ${frontmatter.yaml}
13808
14972
  entry.pane.dispose();
13809
14973
  }
13810
14974
  tabsById.clear();
14975
+ dbFilesCache = null;
13811
14976
  tabsList.innerHTML = "";
13812
14977
  tabHost.innerHTML = "";
13813
14978
  activeTabId = null;
@@ -13846,8 +15011,12 @@ ${frontmatter.yaml}
13846
15011
  if (!mounted)
13847
15012
  return;
13848
15013
  const dbId = parseSseDbId(data);
15014
+ const schema = parseSseSchema(data);
13849
15015
  for (const [, entry] of tabsById) {
13850
- if (dbId && entry.pane.getState().dbId !== dbId)
15016
+ const state = entry.pane.getState();
15017
+ if (dbId && state.dbId !== dbId)
15018
+ continue;
15019
+ if (schema && state.schema !== schema)
13851
15020
  continue;
13852
15021
  entry.pane.handleSse(event, data);
13853
15022
  }
@@ -15417,12 +16586,12 @@ code-viewer annotate add-db --db app.db --tab query \\
15417
16586
  ]
15418
16587
  },
15419
16588
  database: {
15420
- nav: "Database",
15421
- title: "Database Viewer",
15422
- intro: "Browse SQLite files and Docker-hosted MySQL/PostgreSQL databases. Run queries with syntax highlighting and explore table schemas and ER diagrams.",
16589
+ nav: "Datastores",
16590
+ title: "Datastore Viewer",
16591
+ intro: "Browse SQLite files, Docker-hosted databases, Redis, Elasticsearch, and S3-compatible object stores from one local viewer.",
15423
16592
  groups: [
15424
16593
  {
15425
- title: "Supported databases",
16594
+ title: "Supported datastores",
15426
16595
  blocks: [
15427
16596
  {
15428
16597
  kind: "table",
@@ -15438,6 +16607,10 @@ code-viewer annotate add-db --db app.db --tab query \\
15438
16607
  [
15439
16608
  "PostgreSQL",
15440
16609
  "Detected from docker-compose.yml services. Multiple databases per server are listed."
16610
+ ],
16611
+ [
16612
+ "S3 / MinIO / LocalStack",
16613
+ "Detected from docker-compose.yml services. Browse buckets, search objects, sort by update time, and preview common file types."
15441
16614
  ]
15442
16615
  ]
15443
16616
  }
@@ -15780,12 +16953,12 @@ code-viewer annotate add-db --db app.db --tab query \\
15780
16953
  ]
15781
16954
  },
15782
16955
  database: {
15783
- nav: "データベース",
15784
- title: "データベースビューア",
15785
- intro: "SQLite ファイルや Docker 上の MySQL/PostgreSQL を閲覧できます。シンタックスハイライト付きクエリ実行、スキーマ表示、ER 図を提供します。",
16956
+ nav: "データストア",
16957
+ title: "データストアビューア",
16958
+ intro: "SQLite ファイル、Docker 上のデータベース、Redis、Elasticsearch、S3 互換オブジェクトストアをローカルビューアで閲覧できます。",
15786
16959
  groups: [
15787
16960
  {
15788
- title: "対応データベース",
16961
+ title: "対応データストア",
15789
16962
  blocks: [
15790
16963
  {
15791
16964
  kind: "table",
@@ -15801,6 +16974,10 @@ code-viewer annotate add-db --db app.db --tab query \\
15801
16974
  [
15802
16975
  "PostgreSQL",
15803
16976
  "docker-compose.yml のサービスから検出。同一サーバー上の複数データベースを一覧表示します。"
16977
+ ],
16978
+ [
16979
+ "S3 / MinIO / LocalStack",
16980
+ "docker-compose.yml のサービスから検出。バケット閲覧、オブジェクト検索、更新日時順表示、主要ファイル形式のプレビューに対応します。"
15804
16981
  ]
15805
16982
  ]
15806
16983
  }
@@ -15854,7 +17031,7 @@ code-viewer annotate add-db --db app.db --tab query \\
15854
17031
  },
15855
17032
  {
15856
17033
  kind: "command",
15857
- title: "データベース一覧",
17034
+ title: "データストア一覧",
15858
17035
  command: "code-viewer query list"
15859
17036
  },
15860
17037
  {
@@ -18665,7 +19842,7 @@ code-viewer annotate add-db --db app.db --tab query \\
18665
19842
  setRoute,
18666
19843
  currentRange,
18667
19844
  appendScopeParams,
18668
- isAbortError,
19845
+ isAbortError: isAbortError2,
18669
19846
  scrollToFile,
18670
19847
  applySourceRouteToShell,
18671
19848
  fileSourceTarget,
@@ -19012,7 +20189,7 @@ code-viewer annotate add-db --db app.db --tab query \\
19012
20189
  state.status.textContent = response.engine + (state.grepRegex ? " regex" : " plain") + (response.truncated ? " truncated" : "") + " - " + state.items.length + " results";
19013
20190
  renderPalette(state);
19014
20191
  }).catch((err) => {
19015
- if (isAbortError(err))
20192
+ if (isAbortError2(err))
19016
20193
  return;
19017
20194
  state.status.textContent = "Search failed";
19018
20195
  });
@@ -20473,7 +21650,7 @@ code-viewer annotate add-db --db app.db --tab query \\
20473
21650
  setPageMode,
20474
21651
  currentRange,
20475
21652
  trackLoad,
20476
- isAbortError,
21653
+ isAbortError: isAbortError2,
20477
21654
  loadRepo,
20478
21655
  repoRoute,
20479
21656
  repoFileTargetFromRoute,
@@ -20793,16 +21970,6 @@ code-viewer annotate add-db --db app.db --tab query \\
20793
21970
  }
20794
21971
  function renderSourceUnsupported(card, target) {
20795
21972
  const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
20796
- const view = document.createElement("div");
20797
- view.className = "gdp-source-viewer unsupported";
20798
- const content = document.createElement("div");
20799
- content.className = "gdp-source-unsupported-content";
20800
- const title = document.createElement("strong");
20801
- title.className = "gdp-source-unsupported-title";
20802
- title.textContent = "Preview unavailable";
20803
- const message = document.createElement("div");
20804
- message.className = "gdp-source-unsupported-message";
20805
- message.textContent = "This file type cannot be previewed safely in the browser.";
20806
21973
  const info = createSourceFileInfo(target, "unsupported file");
20807
21974
  const link2 = document.createElement("a");
20808
21975
  link2.className = "gdp-btn gdp-btn-sm gdp-source-download";
@@ -20810,23 +21977,17 @@ code-viewer annotate add-db --db app.db --tab query \\
20810
21977
  link2.textContent = "Download raw";
20811
21978
  link2.target = "_blank";
20812
21979
  link2.rel = "noreferrer";
20813
- content.append(title, message, info, link2);
20814
- view.appendChild(content);
21980
+ const view = renderUnsupportedPreview({
21981
+ message: "This file type cannot be previewed safely in the browser.",
21982
+ extraChildren: [info, link2]
21983
+ });
20815
21984
  if (body)
20816
21985
  body.replaceWith(view);
20817
21986
  else
20818
21987
  card.appendChild(view);
20819
21988
  }
20820
21989
  function renderHtmlPreview(target, html) {
20821
- const preview = document.createElement("div");
20822
- preview.className = "gdp-html-preview";
20823
- const frame = document.createElement("iframe");
20824
- frame.title = `${target.path} preview`;
20825
- frame.sandbox.value = "";
20826
- frame.referrerPolicy = "no-referrer";
20827
- frame.srcdoc = html;
20828
- preview.appendChild(frame);
20829
- return preview;
21990
+ return renderHtmlPreviewFrame(`${target.path} preview`, html);
20830
21991
  }
20831
21992
  function createSourceFileInfo(target, kind) {
20832
21993
  const info = document.createElement("div");
@@ -21523,7 +22684,7 @@ code-viewer annotate add-db --db app.db --tab query \\
21523
22684
  renderedEnd = -1;
21524
22685
  render();
21525
22686
  }).catch((err) => {
21526
- if (!isAbortError(err)) {
22687
+ if (!isAbortError2(err)) {
21527
22688
  failedPages.add(page);
21528
22689
  renderedStart = -1;
21529
22690
  renderedEnd = -1;
@@ -21715,35 +22876,16 @@ code-viewer annotate add-db --db app.db --tab query \\
21715
22876
  const url = buildRawFileUrl(target);
21716
22877
  const info = createSourceFileInfo(target, mediaKind);
21717
22878
  view.appendChild(info);
21718
- if (mediaKind === "video") {
21719
- const video = document.createElement("video");
21720
- video.src = url;
21721
- video.controls = true;
21722
- video.preload = "metadata";
21723
- view.appendChild(video);
21724
- } else if (mediaKind === "audio") {
21725
- const audio = document.createElement("audio");
21726
- audio.src = url;
21727
- audio.controls = true;
21728
- audio.preload = "metadata";
21729
- view.appendChild(audio);
21730
- } else if (mediaKind === "pdf") {
21731
- const frame = document.createElement("iframe");
21732
- frame.src = url;
21733
- frame.title = target.path;
21734
- frame.loading = "lazy";
21735
- view.appendChild(frame);
21736
- } else {
21737
- const img = document.createElement("img");
21738
- img.src = url;
21739
- img.alt = "";
21740
- img.addEventListener("load", () => {
22879
+ appendMediaEmbed(view, {
22880
+ url,
22881
+ kind: mediaKind,
22882
+ title: target.path,
22883
+ onImageLoad: (img) => {
21741
22884
  const resolution = document.createElement("span");
21742
22885
  resolution.textContent = `${img.naturalWidth} x ${img.naturalHeight}`;
21743
22886
  info.appendChild(resolution);
21744
- }, { once: true });
21745
- view.appendChild(img);
21746
- }
22887
+ }
22888
+ });
21747
22889
  if (body)
21748
22890
  body.replaceWith(view);
21749
22891
  else
@@ -21916,7 +23058,7 @@ code-viewer annotate add-db --db app.db --tab query \\
21916
23058
  if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
21917
23059
  return;
21918
23060
  finishSourceLoad(req);
21919
- if (isAbortError(err)) {
23061
+ if (isAbortError2(err)) {
21920
23062
  renderSourceCancelled(card, target);
21921
23063
  return;
21922
23064
  }
@@ -22439,7 +23581,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22439
23581
  setPageMode,
22440
23582
  currentRange,
22441
23583
  trackLoad,
22442
- isAbortError,
23584
+ isAbortError: isAbortError2,
22443
23585
  loadRepo: () => REPO_VIEW.loadRepo(),
22444
23586
  repoRoute: (ref, path) => REPO_VIEW.repoRoute(ref, path),
22445
23587
  repoFileTargetFromRoute,
@@ -22523,7 +23665,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22523
23665
  setRoute,
22524
23666
  currentRange,
22525
23667
  appendScopeParams,
22526
- isAbortError,
23668
+ isAbortError: isAbortError2,
22527
23669
  scrollToFile: (path, line) => DIFF_VIEW.scrollToFile(path, line),
22528
23670
  applySourceRouteToShell,
22529
23671
  fileSourceTarget,
@@ -22539,7 +23681,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22539
23681
  repo: "Repository",
22540
23682
  diff: "Diff Viewer",
22541
23683
  history: "History",
22542
- database: "Database",
23684
+ database: "Datastores",
22543
23685
  help: "Help"
22544
23686
  },
22545
23687
  global: {
@@ -22599,7 +23741,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22599
23741
  display: "Display",
22600
23742
  language: "Language",
22601
23743
  fileListFontSize: "UI font size",
22602
- fileListFontSizeHelp: "Applies to the file sidebar and database UI.",
23744
+ fileListFontSizeHelp: "Applies to the file sidebar and datastore UI.",
22603
23745
  codeFontSize: "Code font size",
22604
23746
  sizeSmall: "Small",
22605
23747
  sizeRegular: "Regular",
@@ -22629,7 +23771,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22629
23771
  repo: "リポジトリ",
22630
23772
  diff: "Diff ビューア",
22631
23773
  history: "履歴",
22632
- database: "データベース",
23774
+ database: "データストア",
22633
23775
  help: "ヘルプ"
22634
23776
  },
22635
23777
  global: {
@@ -22689,7 +23831,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22689
23831
  display: "表示",
22690
23832
  language: "言語",
22691
23833
  fileListFontSize: "UIの文字サイズ",
22692
- fileListFontSizeHelp: "ファイル一覧とデータベース画面に適用されます。",
23834
+ fileListFontSizeHelp: "ファイル一覧とデータストア画面に適用されます。",
22693
23835
  codeFontSize: "コード表示の文字サイズ",
22694
23836
  sizeSmall: "小",
22695
23837
  sizeRegular: "標準",
@@ -23106,7 +24248,7 @@ code-viewer annotate add-db --db app.db --tab query \\
23106
24248
  "'": "&#39;"
23107
24249
  })[c2]);
23108
24250
  }
23109
- function isAbortError(err) {
24251
+ function isAbortError2(err) {
23110
24252
  return err instanceof DOMException ? err.name === "AbortError" : !!err && typeof err === "object" && ("name" in err) && err.name === "AbortError";
23111
24253
  }
23112
24254
  function currentRange() {
@@ -23789,7 +24931,7 @@ code-viewer annotate add-db --db app.db --tab query \\
23789
24931
  return Promise.resolve(null);
23790
24932
  }
23791
24933
  if (STATE.route.screen === "database") {
23792
- DATABASE_VIEW.enter(STATE.route.db, STATE.route.table, STATE.route.tab).then(() => ANNOTATIONS_UI?.applyInlineAnnotations());
24934
+ DATABASE_VIEW.enter(STATE.route.db, STATE.route.schema, STATE.route.table, STATE.route.tab).then(() => ANNOTATIONS_UI?.applyInlineAnnotations());
23793
24935
  setStatus("live");
23794
24936
  return Promise.resolve(null);
23795
24937
  }
@@ -23860,7 +25002,7 @@ code-viewer annotate add-db --db app.db --tab query \\
23860
25002
  HISTORY_VIEW.enterHistory();
23861
25003
  } else if (STATE.route.screen === "database") {
23862
25004
  setStatus("live");
23863
- DATABASE_VIEW.enter(STATE.route.db, STATE.route.table, STATE.route.tab).then(() => ANNOTATIONS_UI?.applyInlineAnnotations());
25005
+ DATABASE_VIEW.enter(STATE.route.db, STATE.route.schema, STATE.route.table, STATE.route.tab).then(() => ANNOTATIONS_UI?.applyInlineAnnotations());
23864
25006
  } else
23865
25007
  load();
23866
25008
  syncLineRefPill();
@@ -24010,7 +25152,7 @@ code-viewer annotate add-db --db app.db --tab query \\
24010
25152
  cancelActiveSourceLoad("navigation");
24011
25153
  setPageMode();
24012
25154
  removeStandaloneSource();
24013
- DATABASE_VIEW.enter(STATE.route.db, STATE.route.table, STATE.route.tab).then(() => ANNOTATIONS_UI?.applyInlineAnnotations());
25155
+ DATABASE_VIEW.enter(STATE.route.db, STATE.route.schema, STATE.route.table, STATE.route.tab).then(() => ANNOTATIONS_UI?.applyInlineAnnotations());
24014
25156
  setStatus("live");
24015
25157
  return;
24016
25158
  }
@@ -24128,7 +25270,7 @@ code-viewer annotate add-db --db app.db --tab query \\
24128
25270
  },
24129
25271
  openDatabaseAnnotation: (target) => {
24130
25272
  setStatus("live");
24131
- return DATABASE_VIEW.enter(target.db, target.table, target.tab, {
25273
+ return DATABASE_VIEW.enter(target.db, target.schema, target.table, target.tab, {
24132
25274
  annotationTarget: target,
24133
25275
  reuseActiveTab: true
24134
25276
  });