@youtyan/code-viewer 0.2.4 → 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
@@ -6700,7 +6700,7 @@
6700
6700
  }
6701
6701
  return resolved.join("/");
6702
6702
  }
6703
- function createMarkdownIt(target, highlighter, signal) {
6703
+ function createMarkdownIt(target, highlighter, signal, resolveAssetUrl) {
6704
6704
  const md = new lib_default({
6705
6705
  html: false,
6706
6706
  linkify: true,
@@ -6765,8 +6765,11 @@
6765
6765
  const token = tokens[idx];
6766
6766
  const src = token.attrGet("src") || "";
6767
6767
  const resolved = resolveMarkdownAssetPath(target.path, src);
6768
- if (resolved)
6769
- 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
+ }
6770
6773
  token.attrSet("loading", "lazy");
6771
6774
  return image2(tokens, idx, options, env, self);
6772
6775
  };
@@ -6796,7 +6799,7 @@
6796
6799
  markdown.className = "gdp-markdown-preview markdown-body";
6797
6800
  if (options.signal?.aborted)
6798
6801
  return markdown;
6799
- markdown.innerHTML = renderMarkdownHtml(textValue, target, highlighter, options.signal);
6802
+ markdown.innerHTML = renderMarkdownHtml(textValue, target, highlighter, options.signal, options.resolveAssetUrl);
6800
6803
  if (options.signal?.aborted)
6801
6804
  return markdown;
6802
6805
  enhanceTaskLists(markdown);
@@ -6812,8 +6815,8 @@
6812
6815
  wireMarkdownInteractions(markdown, target, options);
6813
6816
  return markdown;
6814
6817
  }
6815
- function renderMarkdownHtml(textValue, target, highlighter, signal) {
6816
- const md = createMarkdownIt(target, highlighter, signal);
6818
+ function renderMarkdownHtml(textValue, target, highlighter, signal, resolveAssetUrl) {
6819
+ const md = createMarkdownIt(target, highlighter, signal, resolveAssetUrl);
6817
6820
  const frontmatter = splitYamlFrontmatter(textValue);
6818
6821
  if (!frontmatter)
6819
6822
  return md.render(textValue);
@@ -7304,7 +7307,7 @@ ${frontmatter.yaml}
7304
7307
  }
7305
7308
  function annotationLocationLabel(entry) {
7306
7309
  if (entry.target?.kind === "database") {
7307
- const parts = ["Database"];
7310
+ const parts = ["Datastores"];
7308
7311
  if (entry.target.db)
7309
7312
  parts.push(entry.target.db);
7310
7313
  if (entry.target.schema)
@@ -7412,7 +7415,7 @@ ${frontmatter.yaml}
7412
7415
  return;
7413
7416
  const strip = document.createElement("section");
7414
7417
  strip.className = "gdp-db-annotation-strip";
7415
- strip.setAttribute("aria-label", "Database annotations");
7418
+ strip.setAttribute("aria-label", "Datastore annotations");
7416
7419
  for (const entry of matches) {
7417
7420
  strip.appendChild(buildDatabaseAnnotationBlock(entry));
7418
7421
  }
@@ -7655,7 +7658,7 @@ ${frontmatter.yaml}
7655
7658
  return firstLine.length > 90 ? `${firstLine.slice(0, 90)}…` : firstLine;
7656
7659
  }
7657
7660
  function databaseAnnotationTitle(target) {
7658
- const parts = [target.table || target.schema || target.db || "Database"];
7661
+ const parts = [target.table || target.schema || target.db || "Datastores"];
7659
7662
  if (target.schema && target.table)
7660
7663
  parts.unshift(target.schema);
7661
7664
  if (target.tab === "data" && target.data?.search)
@@ -7669,7 +7672,7 @@ ${frontmatter.yaml}
7669
7672
  return parts.join(" / ");
7670
7673
  }
7671
7674
  function openDatabaseCaptureForm(target) {
7672
- $("#annotation-detail-session").textContent = activeSessionId || "Database annotations";
7675
+ $("#annotation-detail-session").textContent = activeSessionId || "Datastore annotations";
7673
7676
  $("#annotation-detail-step").textContent = "new";
7674
7677
  const location2 = $("#annotation-detail-location");
7675
7678
  location2.textContent = databaseAnnotationTitle(target);
@@ -7710,7 +7713,7 @@ ${frontmatter.yaml}
7710
7713
  body: JSON.stringify({
7711
7714
  action: "add",
7712
7715
  session_id: activeSessionId || undefined,
7713
- session_title: activeSessionId ? undefined : "Database annotations",
7716
+ session_title: activeSessionId ? undefined : "Datastore annotations",
7714
7717
  target,
7715
7718
  title: titleInput.value,
7716
7719
  body: bodyInput.value
@@ -8148,6 +8151,35 @@ ${frontmatter.yaml}
8148
8151
  return `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
8149
8152
  }
8150
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
+
8151
8183
  // web-src/views/media-embed.ts
8152
8184
  var MEDIA_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|mp4|webm|mov|mp3|wav|ogg|flac|m4a|aac|opus)(\?.*)?$/i;
8153
8185
  var IMAGE_RE = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)(\?.*)?$/i;
@@ -8591,35 +8623,6 @@ ${frontmatter.yaml}
8591
8623
  return fallback.charAt(0).toUpperCase() + fallback.slice(1);
8592
8624
  }
8593
8625
 
8594
- // web-src/views/database/abort-guard.ts
8595
- function createAbortGuard() {
8596
- let active = null;
8597
- let runId = 0;
8598
- return {
8599
- start() {
8600
- active?.abort("abort-guard:start");
8601
- const abort = new AbortController;
8602
- active = abort;
8603
- const requestRunId = ++runId;
8604
- return {
8605
- signal: abort.signal,
8606
- isStale() {
8607
- return abort.signal.aborted || requestRunId !== runId;
8608
- },
8609
- finish() {
8610
- if (active === abort)
8611
- active = null;
8612
- }
8613
- };
8614
- },
8615
- dispose() {
8616
- runId++;
8617
- active?.abort("abort-guard:dispose");
8618
- active = null;
8619
- }
8620
- };
8621
- }
8622
-
8623
8626
  // web-src/views/database/pane-status.ts
8624
8627
  function setPaneStatus(el, message, options = {}) {
8625
8628
  el.innerHTML = "";
@@ -8703,6 +8706,10 @@ ${frontmatter.yaml}
8703
8706
  let suppressNotify = false;
8704
8707
  let queryNotifyTimer = null;
8705
8708
  let disposed = false;
8709
+ let activeIndexRow = null;
8710
+ let activeDocRow = null;
8711
+ const indexRowsByName = new Map;
8712
+ const docRowsById = new Map;
8706
8713
  const indexGuard = createAbortGuard();
8707
8714
  const mappingGuard = createAbortGuard();
8708
8715
  const docsGuard = createAbortGuard();
@@ -8728,14 +8735,17 @@ ${frontmatter.yaml}
8728
8735
  }
8729
8736
  function renderIndices(indices) {
8730
8737
  indexList.innerHTML = "";
8738
+ indexRowsByName.clear();
8731
8739
  if (indices.length === 0) {
8732
8740
  setIndexStatus("(no indices)");
8733
8741
  return;
8734
8742
  }
8743
+ const fragment = document.createDocumentFragment();
8735
8744
  for (const ix of indices) {
8736
8745
  const item = document.createElement("div");
8737
8746
  item.className = "es-index-item";
8738
8747
  item.dataset.indexName = ix.name;
8748
+ indexRowsByName.set(ix.name, item);
8739
8749
  const name = document.createElement("span");
8740
8750
  name.className = "es-index-name";
8741
8751
  name.textContent = ix.name;
@@ -8744,20 +8754,24 @@ ${frontmatter.yaml}
8744
8754
  meta.className = "es-index-meta";
8745
8755
  meta.textContent = `${ix.docCount.toLocaleString()} docs / ${formatBytes(ix.sizeBytes)}`;
8746
8756
  item.append(name, meta);
8747
- item.addEventListener("click", () => selectIndex(ix.name));
8748
- indexList.appendChild(item);
8757
+ fragment.appendChild(item);
8749
8758
  }
8759
+ indexList.appendChild(fragment);
8750
8760
  }
8751
8761
  function highlightActiveIndex(name) {
8752
- for (const item of indexList.querySelectorAll(".es-index-item")) {
8753
- item.classList.toggle("active", item.dataset.indexName === name);
8754
- }
8762
+ if (activeIndexRow?.dataset.indexName === name)
8763
+ return;
8764
+ activeIndexRow?.classList.remove("active");
8765
+ activeIndexRow = indexRowsByName.get(name) ?? null;
8766
+ activeIndexRow?.classList.add("active");
8755
8767
  }
8756
8768
  function appendDocs(hits) {
8769
+ const fragment = document.createDocumentFragment();
8757
8770
  for (const hit of hits) {
8758
8771
  const row = document.createElement("div");
8759
8772
  row.className = "es-doc-item";
8760
8773
  row.dataset.docId = hit._id;
8774
+ docRowsById.set(hit._id, row);
8761
8775
  const id = document.createElement("span");
8762
8776
  id.className = "es-doc-id";
8763
8777
  id.textContent = hit._id;
@@ -8766,24 +8780,38 @@ ${frontmatter.yaml}
8766
8780
  preview.className = "es-doc-preview";
8767
8781
  preview.textContent = previewSource(hit._source);
8768
8782
  row.append(id, preview);
8769
- row.addEventListener("click", () => selectDoc(hit._id));
8770
- docList.appendChild(row);
8783
+ fragment.appendChild(row);
8771
8784
  }
8785
+ docList.appendChild(fragment);
8772
8786
  }
8773
8787
  function previewSource(src) {
8774
8788
  if (src === null || src === undefined)
8775
8789
  return "";
8776
- try {
8777
- const s2 = JSON.stringify(src);
8778
- return s2.length > 200 ? `${s2.slice(0, 200)}…` : s2;
8779
- } catch {
8780
- 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(" / ");
8781
8805
  }
8806
+ const text2 = String(src);
8807
+ return text2.length > 200 ? `${text2.slice(0, 200)}...` : text2;
8782
8808
  }
8783
8809
  function highlightActiveDoc(id) {
8784
- for (const row of docList.querySelectorAll(".es-doc-item")) {
8785
- row.classList.toggle("active", row.dataset.docId === id);
8786
- }
8810
+ if (activeDocRow?.dataset.docId === id)
8811
+ return;
8812
+ activeDocRow?.classList.remove("active");
8813
+ activeDocRow = docRowsById.get(id) ?? null;
8814
+ activeDocRow?.classList.add("active");
8787
8815
  }
8788
8816
  function setDetailTab(tab) {
8789
8817
  detailTab = tab;
@@ -8913,6 +8941,8 @@ ${frontmatter.yaml}
8913
8941
  if (!append) {
8914
8942
  lastSort = undefined;
8915
8943
  setDocStatus("Loading docs...");
8944
+ docRowsById.clear();
8945
+ activeDocRow = null;
8916
8946
  }
8917
8947
  try {
8918
8948
  const params = new URLSearchParams({
@@ -8938,8 +8968,10 @@ ${frontmatter.yaml}
8938
8968
  if (disposed || slot.isStale() || requestRunId !== loadRunId || requestDbId !== currentDbId || requestIndex !== currentIndex || data.index !== requestIndex || requestQuery !== currentQuery) {
8939
8969
  return;
8940
8970
  }
8941
- if (!append)
8971
+ if (!append) {
8942
8972
  docList.innerHTML = "";
8973
+ docRowsById.clear();
8974
+ }
8943
8975
  if (data.hits.length === 0 && !append) {
8944
8976
  setDocStatus("(no docs)");
8945
8977
  } else {
@@ -9039,6 +9071,22 @@ ${frontmatter.yaml}
9039
9071
  }, 300);
9040
9072
  });
9041
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
+ });
9042
9090
  async function load(dbId, initial) {
9043
9091
  if (disposed)
9044
9092
  return;
@@ -9061,6 +9109,9 @@ ${frontmatter.yaml}
9061
9109
  searchInput.value = initial?.query ?? "";
9062
9110
  currentQuery = searchInput.value.trim();
9063
9111
  docList.innerHTML = "";
9112
+ docRowsById.clear();
9113
+ activeIndexRow = null;
9114
+ activeDocRow = null;
9064
9115
  docMoreBtn.hidden = true;
9065
9116
  mappingBody.innerHTML = "";
9066
9117
  mappingBody.textContent = "Select an index to view its mapping.";
@@ -9117,6 +9168,10 @@ ${frontmatter.yaml}
9117
9168
  searchInput.value = "";
9118
9169
  indexList.innerHTML = "";
9119
9170
  docList.innerHTML = "";
9171
+ indexRowsByName.clear();
9172
+ docRowsById.clear();
9173
+ activeIndexRow = null;
9174
+ activeDocRow = null;
9120
9175
  docMoreBtn.hidden = true;
9121
9176
  mappingBody.innerHTML = "";
9122
9177
  mappingBody.textContent = "Select an index to view its mapping.";
@@ -9964,6 +10019,22 @@ ${frontmatter.yaml}
9964
10019
  let entries = [];
9965
10020
  const expandedIds = new Set;
9966
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
+ }
9967
10038
  function armButtonConfirm(button, confirmText, defaultText) {
9968
10039
  if (button.dataset.confirm === "1") {
9969
10040
  button.dataset.confirm = "";
@@ -9980,29 +10051,42 @@ ${frontmatter.yaml}
9980
10051
  }, 3000);
9981
10052
  return false;
9982
10053
  }
9983
- async function refresh() {
9984
- const dbId = callbacks.getDbId();
9985
- const schema = callbacks.getSchema();
9986
- const searchParams = new URLSearchParams;
9987
- if (dbId)
9988
- searchParams.set("db", dbId);
9989
- if (schema)
9990
- searchParams.set("schema", schema);
9991
- const params = searchParams.toString() ? `?${searchParams.toString()}` : "";
9992
- 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 () => {
9993
10064
  const res = await fetch(`/_db/history${params}`);
9994
10065
  if (!res.ok)
9995
10066
  return;
9996
10067
  const state = await res.json();
10068
+ if (currentRefreshParams().key !== refreshKey)
10069
+ return;
9997
10070
  entries = state.entries;
10071
+ lastRefreshKey = refreshKey;
10072
+ lastRefreshAt = Date.now();
9998
10073
  if (selectedEntryId && !entries.some((entry) => entry.id === selectedEntryId)) {
9999
10074
  clearDetail();
10000
10075
  }
10001
10076
  render();
10002
- } catch {}
10077
+ })().catch(() => {});
10078
+ inFlightRefresh = { key: refreshKey, promise };
10079
+ try {
10080
+ await promise;
10081
+ } finally {
10082
+ if (inFlightRefresh?.promise === promise)
10083
+ inFlightRefresh = null;
10084
+ }
10003
10085
  }
10004
10086
  function render() {
10005
10087
  listEl.innerHTML = "";
10088
+ entryRowsById.clear();
10089
+ selectedEntryRow = null;
10006
10090
  if (entries.length === 0) {
10007
10091
  const empty = document.createElement("div");
10008
10092
  empty.className = "db-query-history-empty";
@@ -10010,21 +10094,25 @@ ${frontmatter.yaml}
10010
10094
  listEl.appendChild(empty);
10011
10095
  return;
10012
10096
  }
10097
+ const fragment = document.createDocumentFragment();
10013
10098
  for (const entry of entries) {
10014
- listEl.appendChild(renderEntry(entry));
10099
+ fragment.appendChild(renderEntry(entry));
10015
10100
  }
10101
+ listEl.appendChild(fragment);
10016
10102
  }
10017
10103
  let selectedEntryId = null;
10018
10104
  function clearDetail() {
10019
10105
  selectedEntryId = null;
10106
+ selectedEntryRow?.classList.remove("selected");
10107
+ selectedEntryRow = null;
10020
10108
  detailCol.innerHTML = "";
10021
10109
  detailCol.appendChild(detailPlaceholder);
10022
10110
  }
10023
10111
  function selectEntry(entry) {
10024
10112
  selectedEntryId = entry.id;
10025
- listEl.querySelectorAll(".db-query-history-entry").forEach((el2) => {
10026
- el2.classList.toggle("selected", el2.dataset.id === entry.id);
10027
- });
10113
+ selectedEntryRow?.classList.remove("selected");
10114
+ selectedEntryRow = entryRowsById.get(entry.id) ?? null;
10115
+ selectedEntryRow?.classList.add("selected");
10028
10116
  renderDetail(entry);
10029
10117
  }
10030
10118
  function renderDetail(entry) {
@@ -10081,9 +10169,12 @@ ${frontmatter.yaml}
10081
10169
  function renderEntry(entry) {
10082
10170
  const item = document.createElement("div");
10083
10171
  item.className = "db-query-history-entry";
10084
- if (entry.id === selectedEntryId)
10172
+ if (entry.id === selectedEntryId) {
10085
10173
  item.classList.add("selected");
10174
+ selectedEntryRow = item;
10175
+ }
10086
10176
  item.dataset.id = entry.id;
10177
+ entryRowsById.set(entry.id, item);
10087
10178
  const meta = document.createElement("div");
10088
10179
  meta.className = "db-query-history-entry-meta";
10089
10180
  const byIcon = document.createElement("span");
@@ -10170,8 +10261,9 @@ ${frontmatter.yaml}
10170
10261
  expandedIds.delete(id);
10171
10262
  if (selectedEntryId === id)
10172
10263
  resetDetailCol();
10173
- const itemEl = listEl.querySelector(`.db-query-history-entry[data-id="${CSS.escape(id)}"]`);
10264
+ const itemEl = entryRowsById.get(id);
10174
10265
  if (itemEl) {
10266
+ entryRowsById.delete(id);
10175
10267
  itemEl.remove();
10176
10268
  if (entries.length === 0)
10177
10269
  render();
@@ -10215,11 +10307,13 @@ ${frontmatter.yaml}
10215
10307
  } catch {}
10216
10308
  });
10217
10309
  refreshBtn.addEventListener("click", () => {
10218
- refresh();
10310
+ refresh({ force: true });
10219
10311
  });
10220
10312
  function clear() {
10221
10313
  entries = [];
10222
10314
  expandedIds.clear();
10315
+ lastRefreshKey = null;
10316
+ lastRefreshAt = 0;
10223
10317
  if (clearConfirmTimer) {
10224
10318
  clearTimeout(clearConfirmTimer);
10225
10319
  clearConfirmTimer = null;
@@ -10321,6 +10415,10 @@ ${frontmatter.yaml}
10321
10415
  let keyRunId = 0;
10322
10416
  let suppressNotify = false;
10323
10417
  let disposed = false;
10418
+ let activeDbRow = null;
10419
+ let activeKeyRow = null;
10420
+ const dbRowsByIndex = new Map;
10421
+ const keyRowsByName = new Map;
10324
10422
  const dbGuard = createAbortGuard();
10325
10423
  const keysGuard = createAbortGuard();
10326
10424
  const valueGuard = createAbortGuard();
@@ -10346,10 +10444,13 @@ ${frontmatter.yaml}
10346
10444
  }
10347
10445
  function renderDatabases(databases) {
10348
10446
  dbList.innerHTML = "";
10447
+ dbRowsByIndex.clear();
10448
+ const fragment = document.createDocumentFragment();
10349
10449
  for (const db of databases) {
10350
10450
  const item = document.createElement("div");
10351
10451
  item.className = "redis-db-item";
10352
10452
  item.dataset.dbIndex = String(db.index);
10453
+ dbRowsByIndex.set(db.index, item);
10353
10454
  const name = document.createElement("span");
10354
10455
  name.className = "redis-db-name";
10355
10456
  name.textContent = `db${db.index}`;
@@ -10357,20 +10458,24 @@ ${frontmatter.yaml}
10357
10458
  count.className = "redis-db-count";
10358
10459
  count.textContent = `${db.keyCount.toLocaleString()} keys`;
10359
10460
  item.append(name, count);
10360
- item.addEventListener("click", () => selectDatabase(db.index));
10361
- dbList.appendChild(item);
10461
+ fragment.appendChild(item);
10362
10462
  }
10463
+ dbList.appendChild(fragment);
10363
10464
  }
10364
10465
  function highlightActiveDb(index) {
10365
- for (const item of dbList.querySelectorAll(".redis-db-item")) {
10366
- item.classList.toggle("active", item.dataset.dbIndex === String(index));
10367
- }
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");
10368
10471
  }
10369
10472
  function appendKeys(keys) {
10473
+ const fragment = document.createDocumentFragment();
10370
10474
  for (const k of keys) {
10371
10475
  const row = document.createElement("div");
10372
10476
  row.className = "redis-key-item";
10373
10477
  row.dataset.keyName = k.name;
10478
+ keyRowsByName.set(k.name, row);
10374
10479
  const typeBadge = document.createElement("span");
10375
10480
  typeBadge.className = `redis-type-badge redis-type-${k.type}`;
10376
10481
  typeBadge.textContent = k.type;
@@ -10379,18 +10484,20 @@ ${frontmatter.yaml}
10379
10484
  nameEl.textContent = k.name;
10380
10485
  nameEl.title = k.name;
10381
10486
  row.append(typeBadge, nameEl);
10382
- row.addEventListener("click", () => selectKey(k.name));
10383
- keyList.appendChild(row);
10487
+ fragment.appendChild(row);
10384
10488
  }
10489
+ keyList.appendChild(fragment);
10385
10490
  }
10386
10491
  function highlightActiveKey(name) {
10387
- for (const row of keyList.querySelectorAll(".redis-key-item")) {
10388
- row.classList.toggle("active", row.dataset.keyName === name);
10389
- }
10492
+ if (activeKeyRow?.dataset.keyName === name)
10493
+ return;
10494
+ activeKeyRow?.classList.remove("active");
10495
+ activeKeyRow = keyRowsByName.get(name) ?? null;
10496
+ activeKeyRow?.classList.add("active");
10390
10497
  }
10391
10498
  function makeNotice(message, kind = "info") {
10392
10499
  const div = document.createElement("div");
10393
- div.className = kind === "warn" ? "redis-value-truncation" : "redis-value-info";
10500
+ div.className = kind === "warn" ? "datastore-value-truncation" : "datastore-value-info";
10394
10501
  div.textContent = message;
10395
10502
  return div;
10396
10503
  }
@@ -10548,6 +10655,8 @@ ${frontmatter.yaml}
10548
10655
  notifySelectionChange();
10549
10656
  highlightActiveDb(dbIndex);
10550
10657
  keyList.innerHTML = "";
10658
+ keyRowsByName.clear();
10659
+ activeKeyRow = null;
10551
10660
  mainPane.textContent = "Select a key to view its value.";
10552
10661
  await loadKeys(false);
10553
10662
  }
@@ -10583,8 +10692,11 @@ ${frontmatter.yaml}
10583
10692
  if (disposed || slot.isStale() || requestRunId !== loadRunId || requestDbId !== currentDbId || requestDbIndex !== currentDbIndex || data.dbIndex !== requestDbIndex) {
10584
10693
  return;
10585
10694
  }
10586
- if (!append)
10695
+ if (!append) {
10587
10696
  keyList.innerHTML = "";
10697
+ keyRowsByName.clear();
10698
+ activeKeyRow = null;
10699
+ }
10588
10700
  if (data.keys.length === 0 && !append) {
10589
10701
  setKeyStatus("(no keys)");
10590
10702
  } else {
@@ -10605,6 +10717,23 @@ ${frontmatter.yaml}
10605
10717
  }
10606
10718
  }
10607
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
+ });
10608
10737
  keyFilterForm.addEventListener("submit", (e2) => {
10609
10738
  e2.preventDefault();
10610
10739
  const nextFilter = keyFilterInput.value.trim() || "*";
@@ -10634,6 +10763,9 @@ ${frontmatter.yaml}
10634
10763
  keyFilterInput.value = currentKeyFilter === "*" ? "" : currentKeyFilter;
10635
10764
  currentCursor = "0";
10636
10765
  keyList.innerHTML = "";
10766
+ keyRowsByName.clear();
10767
+ activeDbRow = null;
10768
+ activeKeyRow = null;
10637
10769
  keyMoreBtn.hidden = true;
10638
10770
  mainPane.textContent = "Select a key to view its value.";
10639
10771
  setDbStatus("Loading databases...");
@@ -10653,11 +10785,21 @@ ${frontmatter.yaml}
10653
10785
  if (initial?.dbIndex !== undefined && data.databases.some((d2) => d2.index === initial.dbIndex)) {
10654
10786
  suppressNotify = true;
10655
10787
  try {
10656
- 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);
10657
10797
  if (currentDbId !== dbId)
10658
10798
  return;
10659
- if (initial.key) {
10660
- await selectKey(initial.key);
10799
+ if (valuePromise) {
10800
+ await valuePromise;
10801
+ if (initial.key)
10802
+ highlightActiveKey(initial.key);
10661
10803
  }
10662
10804
  } finally {
10663
10805
  suppressNotify = false;
@@ -10687,6 +10829,10 @@ ${frontmatter.yaml}
10687
10829
  currentCursor = "0";
10688
10830
  dbList.innerHTML = "";
10689
10831
  keyList.innerHTML = "";
10832
+ dbRowsByIndex.clear();
10833
+ keyRowsByName.clear();
10834
+ activeDbRow = null;
10835
+ activeKeyRow = null;
10690
10836
  keyMoreBtn.hidden = true;
10691
10837
  mainPane.textContent = "Select a database to view keys.";
10692
10838
  }
@@ -10704,6 +10850,654 @@ ${frontmatter.yaml}
10704
10850
  return { el: container, load, clear, dispose, getSelection };
10705
10851
  }
10706
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
+
10707
11501
  // web-src/views/database/schema-view.ts
10708
11502
  function createSchemaView() {
10709
11503
  const el = document.createElement("div");
@@ -11546,6 +12340,7 @@ ${frontmatter.yaml}
11546
12340
  let pageCache = new Map;
11547
12341
  let pendingPages = new Map;
11548
12342
  let loadGeneration = 0;
12343
+ let loadController = new AbortController;
11549
12344
  let rafId = 0;
11550
12345
  let statusEl = null;
11551
12346
  let filterTimer = null;
@@ -11646,10 +12441,18 @@ ${frontmatter.yaml}
11646
12441
  detailPanel.hidden = true;
11647
12442
  detailPanel.innerHTML = "";
11648
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
+ }
11649
12452
  function invalidateData() {
11650
12453
  pageCache = new Map;
11651
12454
  pendingPages = new Map;
11652
- loadGeneration++;
12455
+ startNewLoadGeneration();
11653
12456
  viewport.scrollTop = 0;
11654
12457
  resetSelectionAndDetail();
11655
12458
  return ensurePage(0);
@@ -11668,7 +12471,7 @@ ${frontmatter.yaml}
11668
12471
  filterRow.innerHTML = "";
11669
12472
  pageCache = new Map;
11670
12473
  pendingPages = new Map;
11671
- loadGeneration++;
12474
+ startNewLoadGeneration();
11672
12475
  cancelAnimationFrame(rafId);
11673
12476
  if (filterTimer)
11674
12477
  clearTimeout(filterTimer);
@@ -11881,6 +12684,7 @@ ${frontmatter.yaml}
11881
12684
  }
11882
12685
  pageCache = new Map;
11883
12686
  pendingPages = new Map;
12687
+ startNewLoadGeneration();
11884
12688
  resetSelectionAndDetail();
11885
12689
  renderHeader();
11886
12690
  renderViewport();
@@ -11892,8 +12696,9 @@ ${frontmatter.yaml}
11892
12696
  if (pending)
11893
12697
  return pending;
11894
12698
  const gen = loadGeneration;
12699
+ const signal = loadController.signal;
11895
12700
  const filters = collectFilters();
11896
- 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) => {
11897
12702
  if (gen !== loadGeneration)
11898
12703
  return;
11899
12704
  pageCache.set(pageStart, data.rows);
@@ -11902,6 +12707,8 @@ ${frontmatter.yaml}
11902
12707
  updateStatus();
11903
12708
  renderViewport();
11904
12709
  }).catch((err) => {
12710
+ if (gen !== loadGeneration || isAbortError(err))
12711
+ return;
11905
12712
  if (gen === loadGeneration) {
11906
12713
  showError(err instanceof Error ? err.message : String(err));
11907
12714
  }
@@ -12429,6 +13236,9 @@ ${frontmatter.yaml}
12429
13236
  function errorMessage(err) {
12430
13237
  return err instanceof Error ? err.message : String(err);
12431
13238
  }
13239
+ function isAbortError(err) {
13240
+ return err instanceof DOMException && err.name === "AbortError" || err instanceof Error && err.name === "AbortError";
13241
+ }
12432
13242
  function isSqlKind(kind) {
12433
13243
  return kind === "sqlite" || kind === "postgresql" || kind === "mysql";
12434
13244
  }
@@ -12438,6 +13248,8 @@ ${frontmatter.yaml}
12438
13248
  function isSqlView(view) {
12439
13249
  return view === "data" || view === "query" || view === "schema" || view === "er" || view === "search" || view === "snapshot";
12440
13250
  }
13251
+ var HISTORY_SSE_REFRESH_DELAY_MS = 250;
13252
+ var TABLE_SELECT_FETCH_DELAY_MS = 50;
12441
13253
  function computeVisibility(kind, tab, userPrefersHistoryOpen) {
12442
13254
  const sqlMode = isSqlKind(kind);
12443
13255
  const tableScopedTab = tab === "data" || tab === "schema";
@@ -12456,7 +13268,8 @@ ${frontmatter.yaml}
12456
13268
  searchHidden: !sqlMode || tab !== "search",
12457
13269
  snapshotHidden: !sqlMode || tab !== "snapshot",
12458
13270
  redisHidden: kind !== "redis",
12459
- esHidden: kind !== "elasticsearch"
13271
+ esHidden: kind !== "elasticsearch",
13272
+ s3Hidden: kind !== "s3"
12460
13273
  };
12461
13274
  }
12462
13275
  function normalizeViewForDb(view, db) {
@@ -12491,9 +13304,11 @@ ${frontmatter.yaml}
12491
13304
  let currentSchema = initial.schema ?? null;
12492
13305
  let currentTable = initial.table ?? null;
12493
13306
  let loadGeneration = 0;
13307
+ const tableSelectGuard = createAbortGuard();
13308
+ let historyRefreshPending = null;
12494
13309
  const dbSelect = document.createElement("select");
12495
13310
  dbSelect.className = "db-file-select";
12496
- dbSelect.title = "Select database file";
13311
+ dbSelect.title = "Select datastore";
12497
13312
  const schemaSelect = document.createElement("select");
12498
13313
  schemaSelect.className = "db-file-select db-schema-select";
12499
13314
  schemaSelect.title = "Select PostgreSQL schema";
@@ -12506,7 +13321,7 @@ ${frontmatter.yaml}
12506
13321
  const tabData = createInnerTab("Data", true);
12507
13322
  const tabSchema = createInnerTab("Schema", false);
12508
13323
  tabBar.append(tabData, tabSchema);
12509
- let currentTab = "data";
13324
+ let currentTab = initial.view ?? "data";
12510
13325
  const tableList = createTableList({
12511
13326
  onSelectTable: (table2) => selectTable(table2),
12512
13327
  onViewCreateTable: (table2) => showDdl(table2),
@@ -12520,7 +13335,7 @@ ${frontmatter.yaml}
12520
13335
  const toolsSection = document.createElement("div");
12521
13336
  toolsSection.className = "db-icon-toolbar";
12522
13337
  toolsSection.setAttribute("role", "toolbar");
12523
- toolsSection.setAttribute("aria-label", "Database tools");
13338
+ toolsSection.setAttribute("aria-label", "Datastore tools");
12524
13339
  const queryBtn = makeIconButton({
12525
13340
  label: "Query",
12526
13341
  title: "Query Editor",
@@ -12579,7 +13394,7 @@ ${frontmatter.yaml}
12579
13394
  window.addEventListener("mouseup", onUp);
12580
13395
  });
12581
13396
  const grid = createTableGrid({
12582
- 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),
12583
13398
  getDbId: () => currentDbInfo?.id || null
12584
13399
  });
12585
13400
  const queryEditor = createQueryEditor({
@@ -12615,9 +13430,13 @@ ${frontmatter.yaml}
12615
13430
  onSelectionChange: () => cb.onStateChange()
12616
13431
  });
12617
13432
  esExplorer.el.hidden = true;
13433
+ const s3Explorer = createS3Explorer({
13434
+ onSelectionChange: () => cb.onStateChange()
13435
+ });
13436
+ s3Explorer.el.hidden = true;
12618
13437
  const mainContent = document.createElement("div");
12619
13438
  mainContent.className = "db-main-content";
12620
- 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);
12621
13440
  queryEditor.el.hidden = true;
12622
13441
  globalSearchView.el.hidden = true;
12623
13442
  snapshotView.el.hidden = true;
@@ -12679,6 +13498,7 @@ ${frontmatter.yaml}
12679
13498
  snapshotView.el.hidden = visibility.snapshotHidden;
12680
13499
  redisExplorer.el.hidden = visibility.redisHidden;
12681
13500
  esExplorer.el.hidden = visibility.esHidden;
13501
+ s3Explorer.el.hidden = visibility.s3Hidden;
12682
13502
  if (!sqlMode) {
12683
13503
  queryBtn.classList.remove("active");
12684
13504
  erBtn.classList.remove("active");
@@ -12686,7 +13506,7 @@ ${frontmatter.yaml}
12686
13506
  snapshotBtn.classList.remove("active");
12687
13507
  }
12688
13508
  historyToggle.classList.toggle("active", userPrefersHistoryOpen);
12689
- if (!visibility.historyPaneHidden)
13509
+ if (!visibility.historyPaneHidden && cb.isActive())
12690
13510
  historyView.refresh();
12691
13511
  }
12692
13512
  applyVisibility();
@@ -12723,6 +13543,33 @@ ${frontmatter.yaml}
12723
13543
  if (list2)
12724
13544
  setPaneStatus(list2, message, options);
12725
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
+ }
12726
13573
  function setActiveTab(tab, updateUrl = true) {
12727
13574
  currentTab = normalizeViewForDb(tab, currentDbInfo);
12728
13575
  const tableScopedTab = currentTab === "data" || currentTab === "schema";
@@ -12758,6 +13605,8 @@ ${frontmatter.yaml}
12758
13605
  showSchema(active.dataset.table);
12759
13606
  });
12760
13607
  async function fetchDbFiles() {
13608
+ if (deps.fetchDbFiles)
13609
+ return deps.fetchDbFiles();
12761
13610
  const res = await deps.trackLoad(fetch("/_db/files"));
12762
13611
  if (!res.ok)
12763
13612
  return { files: [] };
@@ -12803,7 +13652,7 @@ ${frontmatter.yaml}
12803
13652
  }
12804
13653
  return await res.json();
12805
13654
  }
12806
- async function fetchTablePage(table2, offset, limit, sort, filters) {
13655
+ async function fetchTablePage(table2, offset, limit, sort, filters, signal) {
12807
13656
  if (!currentDbInfo)
12808
13657
  throw new Error("no database selected");
12809
13658
  const params = new URLSearchParams({
@@ -12820,7 +13669,7 @@ ${frontmatter.yaml}
12820
13669
  if (filters.length > 0) {
12821
13670
  params.set("filters", JSON.stringify(filters));
12822
13671
  }
12823
- const res = await fetch(`/_db/table?${params}`);
13672
+ const res = await fetch(`/_db/table?${params}`, signal ? { signal } : undefined);
12824
13673
  if (!res.ok) {
12825
13674
  throw new Error(await responseErrorMessage(res, "failed to fetch table"));
12826
13675
  }
@@ -12848,15 +13697,14 @@ ${frontmatter.yaml}
12848
13697
  throw new Error(await responseErrorMessage(res, "failed to execute query"));
12849
13698
  }
12850
13699
  const result = await res.json();
12851
- if (userPrefersHistoryOpen)
12852
- historyView.refresh();
12853
13700
  return result;
12854
13701
  }
12855
- async function selectDb(dbId, explorerInitial, generation = loadGeneration, preferredSchema, preferredTable) {
13702
+ async function selectDb(dbId, explorerInitial, generation = loadGeneration, preferredSchema, preferredTable, targetView) {
12856
13703
  if (generation !== loadGeneration || currentDbInfo?.id !== dbId)
12857
13704
  return;
13705
+ tableSelectGuard.dispose();
12858
13706
  currentTable = null;
12859
- if (currentDbInfo?.kind === "redis" || currentDbInfo?.kind === "elasticsearch") {
13707
+ if (currentDbInfo?.kind === "redis" || currentDbInfo?.kind === "elasticsearch" || currentDbInfo?.kind === "s3") {
12860
13708
  currentSchema = null;
12861
13709
  renderSchemaOptions([], null);
12862
13710
  currentTab = "data";
@@ -12868,10 +13716,16 @@ ${frontmatter.yaml}
12868
13716
  applyVisibility();
12869
13717
  if (currentDbInfo.kind === "redis") {
12870
13718
  esExplorer.clear();
13719
+ s3Explorer.clear();
12871
13720
  await redisExplorer.load(dbId, explorerInitial?.redis);
12872
- } else {
13721
+ } else if (currentDbInfo.kind === "elasticsearch") {
12873
13722
  redisExplorer.clear();
13723
+ s3Explorer.clear();
12874
13724
  await esExplorer.load(dbId, explorerInitial?.es);
13725
+ } else {
13726
+ redisExplorer.clear();
13727
+ esExplorer.clear();
13728
+ await s3Explorer.load(dbId, explorerInitial?.s3);
12875
13729
  }
12876
13730
  if (generation !== loadGeneration || currentDbInfo?.id !== dbId)
12877
13731
  return;
@@ -12880,6 +13734,7 @@ ${frontmatter.yaml}
12880
13734
  }
12881
13735
  redisExplorer.clear();
12882
13736
  esExplorer.clear();
13737
+ s3Explorer.clear();
12883
13738
  applyVisibility();
12884
13739
  tableList.render([]);
12885
13740
  setTableListStatus("Loading schema...");
@@ -12936,10 +13791,13 @@ ${frontmatter.yaml}
12936
13791
  grid.clear();
12937
13792
  schemaView.clear();
12938
13793
  erDiagram.clear();
12939
- setActiveTab("data", false);
13794
+ const normalizedTargetView = normalizeViewForDb(targetView, currentDbInfo);
13795
+ setActiveTab(normalizedTargetView === "schema" ? "schema" : "data", false);
12940
13796
  const initialTable = preferredTable || schema.tables[0]?.name;
12941
- if (initialTable) {
13797
+ if (initialTable && normalizedTargetView === "data") {
12942
13798
  await selectTable(initialTable, generation);
13799
+ } else if (initialTable && normalizedTargetView === "schema") {
13800
+ await selectTableSchemaOnly(initialTable, generation);
12943
13801
  }
12944
13802
  applyVisibility();
12945
13803
  cb.onStateChange();
@@ -12947,6 +13805,7 @@ ${frontmatter.yaml}
12947
13805
  async function selectTable(table2, generation = loadGeneration) {
12948
13806
  if (generation !== loadGeneration)
12949
13807
  return;
13808
+ const slot = tableSelectGuard.start();
12950
13809
  currentTable = table2;
12951
13810
  tableList.setActive(table2);
12952
13811
  if (!currentDbInfo)
@@ -12964,16 +13823,22 @@ ${frontmatter.yaml}
12964
13823
  range: deps.currentRange()
12965
13824
  }, true);
12966
13825
  try {
12967
- const data = await deps.trackLoad(fetchTablePage(table2, 0, 200, null, []));
12968
- 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) {
12969
13832
  return;
12970
13833
  }
12971
13834
  grid.load(table2, data);
12972
13835
  } catch (err) {
12973
- if (generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
13836
+ if (slot.isStale() || isAbortError(err) || generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
12974
13837
  return;
12975
13838
  }
12976
13839
  grid.showError(errorMessage(err));
13840
+ } finally {
13841
+ slot.finish();
12977
13842
  }
12978
13843
  if (currentTab === "schema") {
12979
13844
  const columns = await fetchColumns(table2);
@@ -12983,6 +13848,21 @@ ${frontmatter.yaml}
12983
13848
  schemaView.render(table2, columns, schemaCache?.indexes || []);
12984
13849
  }
12985
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
+ }
12986
13866
  async function fetchColumns(table2) {
12987
13867
  if (schemaCache?.columnsMap?.[table2]) {
12988
13868
  return schemaCache.columnsMap[table2];
@@ -13098,6 +13978,7 @@ ${frontmatter.yaml}
13098
13978
  }
13099
13979
  let pendingRedisInitial = initial.redis;
13100
13980
  let pendingEsInitial = initial.es;
13981
+ let pendingS3Initial = initial.s3;
13101
13982
  async function enter(db, schema, table2, view, options = {}) {
13102
13983
  const generation = ++loadGeneration;
13103
13984
  const filesResponse = await fetchDbFiles();
@@ -13114,21 +13995,23 @@ ${frontmatter.yaml}
13114
13995
  if (files.length === 0) {
13115
13996
  const opt = document.createElement("option");
13116
13997
  opt.value = "";
13117
- opt.textContent = "No database files found";
13998
+ opt.textContent = "No datastores found";
13118
13999
  dbSelect.appendChild(opt);
13119
14000
  dbSelect.disabled = true;
13120
14001
  cb.onStateChange();
13121
14002
  return;
13122
14003
  }
13123
14004
  dbSelect.disabled = false;
14005
+ const optionsFragment = document.createDocumentFragment();
13124
14006
  for (const f2 of files) {
13125
14007
  const opt = document.createElement("option");
13126
14008
  opt.value = f2.id;
13127
14009
  const isDocker = f2.id.startsWith("docker:");
13128
14010
  const label = isDocker ? `${f2.name} (Docker)` : `${f2.path} (${formatSize(f2.sizeBytes)})`;
13129
14011
  opt.textContent = label;
13130
- dbSelect.appendChild(opt);
14012
+ optionsFragment.appendChild(opt);
13131
14013
  }
14014
+ dbSelect.appendChild(optionsFragment);
13132
14015
  const autoSelectFirst = options.autoSelectFirst ?? true;
13133
14016
  if (db && !files.find((f2) => f2.id === db)) {
13134
14017
  db = autoSelectFirst ? files[0].id : null;
@@ -13146,6 +14029,7 @@ ${frontmatter.yaml}
13146
14029
  erDiagram.clear();
13147
14030
  redisExplorer.clear();
13148
14031
  esExplorer.clear();
14032
+ s3Explorer.clear();
13149
14033
  setActiveTab("data", false);
13150
14034
  cb.onStateChange();
13151
14035
  return;
@@ -13155,14 +14039,16 @@ ${frontmatter.yaml}
13155
14039
  currentDbInfo = files.find((f2) => f2.id === target) || null;
13156
14040
  const explorerInitial = {
13157
14041
  redis: pendingRedisInitial,
13158
- es: pendingEsInitial
14042
+ es: pendingEsInitial,
14043
+ s3: pendingS3Initial
13159
14044
  };
13160
14045
  pendingRedisInitial = undefined;
13161
14046
  pendingEsInitial = undefined;
13162
- await selectDb(target, explorerInitial, generation, schema !== undefined ? schema : currentSchema, table2);
14047
+ pendingS3Initial = undefined;
14048
+ await selectDb(target, explorerInitial, generation, schema !== undefined ? schema : currentSchema, table2, view);
13163
14049
  if (generation !== loadGeneration || currentDbInfo?.id !== target)
13164
14050
  return;
13165
- if (currentDbInfo?.kind === "redis" || currentDbInfo?.kind === "elasticsearch") {
14051
+ if (currentDbInfo?.kind === "redis" || currentDbInfo?.kind === "elasticsearch" || currentDbInfo?.kind === "s3") {
13166
14052
  cb.onStateChange();
13167
14053
  return;
13168
14054
  }
@@ -13219,20 +14105,28 @@ ${frontmatter.yaml}
13219
14105
  }
13220
14106
  }
13221
14107
  function handleSse(event, data) {
13222
- if (userPrefersHistoryOpen)
13223
- 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
+ }
13224
14118
  if (event === "db-snapshot" && data) {
13225
14119
  snapshotView.handleSse(data);
13226
14120
  }
13227
14121
  }
13228
14122
  function getState() {
13229
- const activeTable = tableList.el.querySelector(".db-table-item.active");
14123
+ const loaded = !!currentDbInfo;
13230
14124
  const state = {
13231
14125
  id: cb.tabId,
13232
- dbId: currentDbInfo?.id ?? null,
13233
- schema: currentSchema,
13234
- table: currentTable ?? activeTable?.dataset.table ?? null,
13235
- 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
13236
14130
  };
13237
14131
  const sqlDraft = queryEditor.getSql();
13238
14132
  if (sqlDraft)
@@ -13243,7 +14137,14 @@ ${frontmatter.yaml}
13243
14137
  state.historyHeight = historyPane.style.height;
13244
14138
  if (sidebar.style.width)
13245
14139
  state.sidebarWidth = sidebar.style.width;
13246
- 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") {
13247
14148
  const sel = redisExplorer.getSelection();
13248
14149
  if (sel.dbIndex !== undefined || sel.key !== undefined || sel.keyFilter !== undefined) {
13249
14150
  state.redis = sel;
@@ -13253,6 +14154,11 @@ ${frontmatter.yaml}
13253
14154
  if (sel.index !== undefined || sel.query !== undefined) {
13254
14155
  state.es = sel;
13255
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
+ }
13256
14162
  }
13257
14163
  return state;
13258
14164
  }
@@ -13290,6 +14196,11 @@ ${frontmatter.yaml}
13290
14196
  }
13291
14197
  function dispose() {
13292
14198
  loadGeneration++;
14199
+ tableSelectGuard.dispose();
14200
+ if (historyRefreshPending !== null) {
14201
+ clearTimeout(historyRefreshPending);
14202
+ historyRefreshPending = null;
14203
+ }
13293
14204
  grid.destroy();
13294
14205
  schemaView.clear();
13295
14206
  erDiagram.dispose();
@@ -13300,6 +14211,7 @@ ${frontmatter.yaml}
13300
14211
  historyView.clear();
13301
14212
  redisExplorer.dispose();
13302
14213
  esExplorer.dispose();
14214
+ s3Explorer.dispose();
13303
14215
  currentDbInfo = null;
13304
14216
  currentSchema = null;
13305
14217
  currentTable = null;
@@ -13343,6 +14255,7 @@ ${frontmatter.yaml}
13343
14255
  let mounted = false;
13344
14256
  const tabsById = new Map;
13345
14257
  const paneReadyById = new Map;
14258
+ const lazyInitialById = new Map;
13346
14259
  let activeTabId = null;
13347
14260
  let draggingTabId = null;
13348
14261
  let dropTargetId = null;
@@ -13350,9 +14263,40 @@ ${frontmatter.yaml}
13350
14263
  let restoring = false;
13351
14264
  let savePending = null;
13352
14265
  let saveChain = Promise.resolve();
14266
+ let lastSavedTabsRaw = null;
14267
+ let pendingSavedTabsRaw = null;
14268
+ let saveController = null;
13353
14269
  let lifecycleSeq = 0;
13354
14270
  let enterQueue = Promise.resolve();
13355
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
+ }
13356
14300
  function scheduleSave() {
13357
14301
  if (!mounted || restoring)
13358
14302
  return;
@@ -13360,6 +14304,10 @@ ${frontmatter.yaml}
13360
14304
  clearTimeout(savePending);
13361
14305
  savePending = setTimeout(saveNow, 500);
13362
14306
  }
14307
+ function abortActiveSave() {
14308
+ saveController?.abort();
14309
+ saveController = null;
14310
+ }
13363
14311
  async function saveNow(options = {}) {
13364
14312
  savePending = null;
13365
14313
  if (!mounted || restoring)
@@ -13372,7 +14320,33 @@ ${frontmatter.yaml}
13372
14320
  return;
13373
14321
  const body = { version: 1, tabs, activeTabId };
13374
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
+ }
13375
14347
  saveChain = saveChain.catch(() => {}).then(async () => {
14348
+ const controller = new AbortController;
14349
+ saveController = controller;
13376
14350
  try {
13377
14351
  await fetch("/_db/tabs", {
13378
14352
  method: "PUT",
@@ -13381,9 +14355,17 @@ ${frontmatter.yaml}
13381
14355
  "X-Code-Viewer-Action": "1"
13382
14356
  },
13383
14357
  body: raw,
13384
- keepalive: options.keepalive
14358
+ signal: controller.signal
13385
14359
  });
13386
- } 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
+ }
13387
14369
  });
13388
14370
  await saveChain;
13389
14371
  }
@@ -13436,6 +14418,9 @@ ${frontmatter.yaml}
13436
14418
  if (!restoring)
13437
14419
  syncActiveRoute();
13438
14420
  scheduleSave();
14421
+ if (mounted && !restoring) {
14422
+ ensureInitialEnter(id)?.catch(() => {});
14423
+ }
13439
14424
  }
13440
14425
  function syncActiveRoute() {
13441
14426
  if (!activeTabId)
@@ -13472,7 +14457,8 @@ ${frontmatter.yaml}
13472
14457
  table: tab.table,
13473
14458
  view: tab.view,
13474
14459
  redis: tab.redis ?? null,
13475
- es: tab.es ?? null
14460
+ es: tab.es ?? null,
14461
+ s3: tab.s3 ?? null
13476
14462
  });
13477
14463
  if (seen.has(key))
13478
14464
  continue;
@@ -13687,7 +14673,7 @@ ${frontmatter.yaml}
13687
14673
  closeTab(id);
13688
14674
  }
13689
14675
  });
13690
- const pane = createTabPane(deps, {
14676
+ const pane = createTabPane({ ...deps, fetchDbFiles: fetchDbFilesCached }, {
13691
14677
  tabId: id,
13692
14678
  isActive: () => activeTabId === id,
13693
14679
  canSyncRoute: () => !restoring,
@@ -13707,13 +14693,15 @@ ${frontmatter.yaml}
13707
14693
  historyHeight: initial?.historyHeight,
13708
14694
  sidebarWidth: initial?.sidebarWidth,
13709
14695
  redis: initial?.redis,
13710
- es: initial?.es
14696
+ es: initial?.es,
14697
+ s3: initial?.s3
13711
14698
  });
13712
14699
  pane.el.hidden = true;
13713
14700
  tabsList.appendChild(chip);
13714
14701
  tabHost.appendChild(pane.el);
13715
14702
  tabsById.set(id, { pane, chip, label: labelEl, closeBtn });
13716
- setActive(id);
14703
+ if (options.activate !== false)
14704
+ setActive(id);
13717
14705
  if (!options.deferInitialEnter) {
13718
14706
  startInitialEnter(id, initial, options);
13719
14707
  }
@@ -13739,6 +14727,16 @@ ${frontmatter.yaml}
13739
14727
  paneReadyById.set(id, ready);
13740
14728
  return ready;
13741
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
+ }
13742
14740
  function closeTab(id) {
13743
14741
  const entry = tabsById.get(id);
13744
14742
  if (!entry)
@@ -13749,6 +14747,7 @@ ${frontmatter.yaml}
13749
14747
  entry.chip.remove();
13750
14748
  tabsById.delete(id);
13751
14749
  paneReadyById.delete(id);
14750
+ lazyInitialById.delete(id);
13752
14751
  closeDbIfUnused(closedDbId);
13753
14752
  if (activeTabId !== id) {
13754
14753
  scheduleSave();
@@ -13798,6 +14797,11 @@ ${frontmatter.yaml}
13798
14797
  for (const [id2, entry] of tabsById) {
13799
14798
  if (routeMatchesState(entry.pane.getState(), db, schema, table2, view)) {
13800
14799
  setActive(id2);
14800
+ const ready2 = ensureInitialEnter(id2);
14801
+ if (ready2)
14802
+ await ready2;
14803
+ if (!mounted)
14804
+ return;
13801
14805
  if (options.annotationTarget) {
13802
14806
  await enterPane(entry.pane, db, schema, table2, view, options);
13803
14807
  if (!mounted)
@@ -13878,8 +14882,6 @@ ${frontmatter.yaml}
13878
14882
  return;
13879
14883
  if (restored && restored.tabs.length > 0) {
13880
14884
  restoring = true;
13881
- const restoredIds = [];
13882
- const restoredById = new Map;
13883
14885
  try {
13884
14886
  const restoredTabs = dedupeTabs(restored.tabs);
13885
14887
  for (const t2 of restoredTabs) {
@@ -13887,28 +14889,21 @@ ${frontmatter.yaml}
13887
14889
  return;
13888
14890
  const id = openTab(t2, {
13889
14891
  autoSelectFirst: false,
13890
- deferInitialEnter: true
14892
+ deferInitialEnter: true,
14893
+ activate: false
13891
14894
  });
13892
- restoredIds.push(id);
13893
- restoredById.set(id, t2);
14895
+ lazyInitialById.set(id, t2);
13894
14896
  }
13895
14897
  const targetId = restored.activeTabId && tabsById.has(restored.activeTabId) ? restored.activeTabId : tabsById.keys().next().value;
13896
14898
  if (targetId)
13897
14899
  setActive(targetId);
13898
14900
  if (targetId) {
13899
- await startInitialEnter(targetId, restoredById.get(targetId), {
13900
- autoSelectFirst: false
13901
- });
14901
+ const ready = ensureInitialEnter(targetId);
14902
+ if (ready)
14903
+ await ready;
13902
14904
  }
13903
14905
  if (!mounted || seq !== lifecycleSeq)
13904
14906
  return;
13905
- for (const id of restoredIds) {
13906
- if (id === targetId)
13907
- continue;
13908
- startInitialEnter(id, restoredById.get(id), {
13909
- autoSelectFirst: false
13910
- }).catch(() => {});
13911
- }
13912
14907
  } finally {
13913
14908
  restoring = false;
13914
14909
  }
@@ -13977,6 +14972,7 @@ ${frontmatter.yaml}
13977
14972
  entry.pane.dispose();
13978
14973
  }
13979
14974
  tabsById.clear();
14975
+ dbFilesCache = null;
13980
14976
  tabsList.innerHTML = "";
13981
14977
  tabHost.innerHTML = "";
13982
14978
  activeTabId = null;
@@ -15590,12 +16586,12 @@ code-viewer annotate add-db --db app.db --tab query \\
15590
16586
  ]
15591
16587
  },
15592
16588
  database: {
15593
- nav: "Database",
15594
- title: "Database Viewer",
15595
- 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.",
15596
16592
  groups: [
15597
16593
  {
15598
- title: "Supported databases",
16594
+ title: "Supported datastores",
15599
16595
  blocks: [
15600
16596
  {
15601
16597
  kind: "table",
@@ -15611,6 +16607,10 @@ code-viewer annotate add-db --db app.db --tab query \\
15611
16607
  [
15612
16608
  "PostgreSQL",
15613
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."
15614
16614
  ]
15615
16615
  ]
15616
16616
  }
@@ -15953,12 +16953,12 @@ code-viewer annotate add-db --db app.db --tab query \\
15953
16953
  ]
15954
16954
  },
15955
16955
  database: {
15956
- nav: "データベース",
15957
- title: "データベースビューア",
15958
- intro: "SQLite ファイルや Docker 上の MySQL/PostgreSQL を閲覧できます。シンタックスハイライト付きクエリ実行、スキーマ表示、ER 図を提供します。",
16956
+ nav: "データストア",
16957
+ title: "データストアビューア",
16958
+ intro: "SQLite ファイル、Docker 上のデータベース、Redis、Elasticsearch、S3 互換オブジェクトストアをローカルビューアで閲覧できます。",
15959
16959
  groups: [
15960
16960
  {
15961
- title: "対応データベース",
16961
+ title: "対応データストア",
15962
16962
  blocks: [
15963
16963
  {
15964
16964
  kind: "table",
@@ -15974,6 +16974,10 @@ code-viewer annotate add-db --db app.db --tab query \\
15974
16974
  [
15975
16975
  "PostgreSQL",
15976
16976
  "docker-compose.yml のサービスから検出。同一サーバー上の複数データベースを一覧表示します。"
16977
+ ],
16978
+ [
16979
+ "S3 / MinIO / LocalStack",
16980
+ "docker-compose.yml のサービスから検出。バケット閲覧、オブジェクト検索、更新日時順表示、主要ファイル形式のプレビューに対応します。"
15977
16981
  ]
15978
16982
  ]
15979
16983
  }
@@ -16027,7 +17031,7 @@ code-viewer annotate add-db --db app.db --tab query \\
16027
17031
  },
16028
17032
  {
16029
17033
  kind: "command",
16030
- title: "データベース一覧",
17034
+ title: "データストア一覧",
16031
17035
  command: "code-viewer query list"
16032
17036
  },
16033
17037
  {
@@ -18838,7 +19842,7 @@ code-viewer annotate add-db --db app.db --tab query \\
18838
19842
  setRoute,
18839
19843
  currentRange,
18840
19844
  appendScopeParams,
18841
- isAbortError,
19845
+ isAbortError: isAbortError2,
18842
19846
  scrollToFile,
18843
19847
  applySourceRouteToShell,
18844
19848
  fileSourceTarget,
@@ -19185,7 +20189,7 @@ code-viewer annotate add-db --db app.db --tab query \\
19185
20189
  state.status.textContent = response.engine + (state.grepRegex ? " regex" : " plain") + (response.truncated ? " truncated" : "") + " - " + state.items.length + " results";
19186
20190
  renderPalette(state);
19187
20191
  }).catch((err) => {
19188
- if (isAbortError(err))
20192
+ if (isAbortError2(err))
19189
20193
  return;
19190
20194
  state.status.textContent = "Search failed";
19191
20195
  });
@@ -20646,7 +21650,7 @@ code-viewer annotate add-db --db app.db --tab query \\
20646
21650
  setPageMode,
20647
21651
  currentRange,
20648
21652
  trackLoad,
20649
- isAbortError,
21653
+ isAbortError: isAbortError2,
20650
21654
  loadRepo,
20651
21655
  repoRoute,
20652
21656
  repoFileTargetFromRoute,
@@ -20966,16 +21970,6 @@ code-viewer annotate add-db --db app.db --tab query \\
20966
21970
  }
20967
21971
  function renderSourceUnsupported(card, target) {
20968
21972
  const body = card.querySelector(".gdp-file-detail-body, .d2h-files-diff, .d2h-file-diff, .gdp-media, .gdp-source-viewer");
20969
- const view = document.createElement("div");
20970
- view.className = "gdp-source-viewer unsupported";
20971
- const content = document.createElement("div");
20972
- content.className = "gdp-source-unsupported-content";
20973
- const title = document.createElement("strong");
20974
- title.className = "gdp-source-unsupported-title";
20975
- title.textContent = "Preview unavailable";
20976
- const message = document.createElement("div");
20977
- message.className = "gdp-source-unsupported-message";
20978
- message.textContent = "This file type cannot be previewed safely in the browser.";
20979
21973
  const info = createSourceFileInfo(target, "unsupported file");
20980
21974
  const link2 = document.createElement("a");
20981
21975
  link2.className = "gdp-btn gdp-btn-sm gdp-source-download";
@@ -20983,23 +21977,17 @@ code-viewer annotate add-db --db app.db --tab query \\
20983
21977
  link2.textContent = "Download raw";
20984
21978
  link2.target = "_blank";
20985
21979
  link2.rel = "noreferrer";
20986
- content.append(title, message, info, link2);
20987
- view.appendChild(content);
21980
+ const view = renderUnsupportedPreview({
21981
+ message: "This file type cannot be previewed safely in the browser.",
21982
+ extraChildren: [info, link2]
21983
+ });
20988
21984
  if (body)
20989
21985
  body.replaceWith(view);
20990
21986
  else
20991
21987
  card.appendChild(view);
20992
21988
  }
20993
21989
  function renderHtmlPreview(target, html) {
20994
- const preview = document.createElement("div");
20995
- preview.className = "gdp-html-preview";
20996
- const frame = document.createElement("iframe");
20997
- frame.title = `${target.path} preview`;
20998
- frame.sandbox.value = "";
20999
- frame.referrerPolicy = "no-referrer";
21000
- frame.srcdoc = html;
21001
- preview.appendChild(frame);
21002
- return preview;
21990
+ return renderHtmlPreviewFrame(`${target.path} preview`, html);
21003
21991
  }
21004
21992
  function createSourceFileInfo(target, kind) {
21005
21993
  const info = document.createElement("div");
@@ -21696,7 +22684,7 @@ code-viewer annotate add-db --db app.db --tab query \\
21696
22684
  renderedEnd = -1;
21697
22685
  render();
21698
22686
  }).catch((err) => {
21699
- if (!isAbortError(err)) {
22687
+ if (!isAbortError2(err)) {
21700
22688
  failedPages.add(page);
21701
22689
  renderedStart = -1;
21702
22690
  renderedEnd = -1;
@@ -21888,35 +22876,16 @@ code-viewer annotate add-db --db app.db --tab query \\
21888
22876
  const url = buildRawFileUrl(target);
21889
22877
  const info = createSourceFileInfo(target, mediaKind);
21890
22878
  view.appendChild(info);
21891
- if (mediaKind === "video") {
21892
- const video = document.createElement("video");
21893
- video.src = url;
21894
- video.controls = true;
21895
- video.preload = "metadata";
21896
- view.appendChild(video);
21897
- } else if (mediaKind === "audio") {
21898
- const audio = document.createElement("audio");
21899
- audio.src = url;
21900
- audio.controls = true;
21901
- audio.preload = "metadata";
21902
- view.appendChild(audio);
21903
- } else if (mediaKind === "pdf") {
21904
- const frame = document.createElement("iframe");
21905
- frame.src = url;
21906
- frame.title = target.path;
21907
- frame.loading = "lazy";
21908
- view.appendChild(frame);
21909
- } else {
21910
- const img = document.createElement("img");
21911
- img.src = url;
21912
- img.alt = "";
21913
- img.addEventListener("load", () => {
22879
+ appendMediaEmbed(view, {
22880
+ url,
22881
+ kind: mediaKind,
22882
+ title: target.path,
22883
+ onImageLoad: (img) => {
21914
22884
  const resolution = document.createElement("span");
21915
22885
  resolution.textContent = `${img.naturalWidth} x ${img.naturalHeight}`;
21916
22886
  info.appendChild(resolution);
21917
- }, { once: true });
21918
- view.appendChild(img);
21919
- }
22887
+ }
22888
+ });
21920
22889
  if (body)
21921
22890
  body.replaceWith(view);
21922
22891
  else
@@ -22089,7 +23058,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22089
23058
  if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
22090
23059
  return;
22091
23060
  finishSourceLoad(req);
22092
- if (isAbortError(err)) {
23061
+ if (isAbortError2(err)) {
22093
23062
  renderSourceCancelled(card, target);
22094
23063
  return;
22095
23064
  }
@@ -22612,7 +23581,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22612
23581
  setPageMode,
22613
23582
  currentRange,
22614
23583
  trackLoad,
22615
- isAbortError,
23584
+ isAbortError: isAbortError2,
22616
23585
  loadRepo: () => REPO_VIEW.loadRepo(),
22617
23586
  repoRoute: (ref, path) => REPO_VIEW.repoRoute(ref, path),
22618
23587
  repoFileTargetFromRoute,
@@ -22696,7 +23665,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22696
23665
  setRoute,
22697
23666
  currentRange,
22698
23667
  appendScopeParams,
22699
- isAbortError,
23668
+ isAbortError: isAbortError2,
22700
23669
  scrollToFile: (path, line) => DIFF_VIEW.scrollToFile(path, line),
22701
23670
  applySourceRouteToShell,
22702
23671
  fileSourceTarget,
@@ -22712,7 +23681,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22712
23681
  repo: "Repository",
22713
23682
  diff: "Diff Viewer",
22714
23683
  history: "History",
22715
- database: "Database",
23684
+ database: "Datastores",
22716
23685
  help: "Help"
22717
23686
  },
22718
23687
  global: {
@@ -22772,7 +23741,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22772
23741
  display: "Display",
22773
23742
  language: "Language",
22774
23743
  fileListFontSize: "UI font size",
22775
- fileListFontSizeHelp: "Applies to the file sidebar and database UI.",
23744
+ fileListFontSizeHelp: "Applies to the file sidebar and datastore UI.",
22776
23745
  codeFontSize: "Code font size",
22777
23746
  sizeSmall: "Small",
22778
23747
  sizeRegular: "Regular",
@@ -22802,7 +23771,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22802
23771
  repo: "リポジトリ",
22803
23772
  diff: "Diff ビューア",
22804
23773
  history: "履歴",
22805
- database: "データベース",
23774
+ database: "データストア",
22806
23775
  help: "ヘルプ"
22807
23776
  },
22808
23777
  global: {
@@ -22862,7 +23831,7 @@ code-viewer annotate add-db --db app.db --tab query \\
22862
23831
  display: "表示",
22863
23832
  language: "言語",
22864
23833
  fileListFontSize: "UIの文字サイズ",
22865
- fileListFontSizeHelp: "ファイル一覧とデータベース画面に適用されます。",
23834
+ fileListFontSizeHelp: "ファイル一覧とデータストア画面に適用されます。",
22866
23835
  codeFontSize: "コード表示の文字サイズ",
22867
23836
  sizeSmall: "小",
22868
23837
  sizeRegular: "標準",
@@ -23279,7 +24248,7 @@ code-viewer annotate add-db --db app.db --tab query \\
23279
24248
  "'": "&#39;"
23280
24249
  })[c2]);
23281
24250
  }
23282
- function isAbortError(err) {
24251
+ function isAbortError2(err) {
23283
24252
  return err instanceof DOMException ? err.name === "AbortError" : !!err && typeof err === "object" && ("name" in err) && err.name === "AbortError";
23284
24253
  }
23285
24254
  function currentRange() {