From 06198ec4db5b4018f3ffba41c4abd80a880f11f9 Mon Sep 17 00:00:00 2001 From: 3x-haust Date: Mon, 15 Jun 2026 22:13:03 +0900 Subject: [PATCH] feat: add GitDB website benchmarks --- README.md | 6 +- docs/README.ko.md | 4 +- examples/api-encrypted/index.mjs | 38 +- examples/api-plaintext/index.mjs | 31 -- site/app.js | 416 ++++++------------ site/benchmark.json | 48 +-- site/benchmark.md | 28 +- site/content.js | 289 ------------- site/favicon.svg | 12 +- site/index.html | 321 ++++++-------- site/styles.css | 715 ++++++++++--------------------- tests/no-postgres-facade.test.ts | 6 +- tests/site.test.ts | 95 ++-- 13 files changed, 589 insertions(+), 1420 deletions(-) delete mode 100644 site/content.js diff --git a/README.md b/README.md index 2133617..8e74474 100644 --- a/README.md +++ b/README.md @@ -204,9 +204,9 @@ GITDB_GITHUB_TOKEN=github_token_with_contents_write_access ## Examples -The repository keeps exactly two live GitHub-backed examples. Both use the -browser-compatible `fetch` store so the same shape applies to extensions and -static apps: +The repository keeps exactly two live GitHub-backed examples. Both configure the +browser-compatible GitHub store adapter, then use only the GitDB table API so the +same shape applies to extensions and static apps: - `examples/api-plaintext` - `examples/api-encrypted` diff --git a/docs/README.ko.md b/docs/README.ko.md index 9f90b72..f16b3b5 100644 --- a/docs/README.ko.md +++ b/docs/README.ko.md @@ -204,8 +204,8 @@ GITDB_GITHUB_TOKEN=github_token_with_contents_write_access ## 예제 레포에는 실제 GitHub DB repository에 쓰는 예제를 두 개만 둡니다. 둘 다 -browser-compatible `fetch` store를 써서 extension/static app과 같은 구조를 -보여줍니다: +browser-compatible GitHub store adapter를 설정한 뒤 GitDB table API만 사용합니다. +그래서 extension/static app과 같은 구조를 보여줍니다: - `examples/api-plaintext` - `examples/api-encrypted` diff --git a/examples/api-encrypted/index.mjs b/examples/api-encrypted/index.mjs index 588a335..49c4d4c 100644 --- a/examples/api-encrypted/index.mjs +++ b/examples/api-encrypted/index.mjs @@ -37,13 +37,6 @@ await accounts.upsertMany([ { id: "acct_2", status: "active", tier: "pro" }, ]) -const remoteManifestPath = `${config.prefix}/manifest.enc` -const remotePayload = await readGitHubFile(config, remoteManifestPath) -const leaksPlaintext = remotePayload.includes("acct_1") || remotePayload.includes("active") -if (leaksPlaintext) { - throw new Error("encrypted GitHub example leaked plaintext into the remote manifest") -} - const activeAccounts = await readRowsFromGitHub(async () => { const verificationDb = await GitDb.open({ store: new GitHubFetchEncryptedStore(config, cipher), @@ -55,11 +48,10 @@ const activeAccounts = await readRowsFromGitHub(async () => { const summary = { activeAccounts, branch: config.branch, + encryption: "web-aes-gcm", example: "api-encrypted", - leaksPlaintext, mode: "github-encrypted", prefix: config.prefix, - remoteManifestPath, remoteVerified: true, repo: `${config.owner}/${config.repo}`, status: "ok", @@ -86,34 +78,6 @@ function requiredEnv(name, fallback) { return value } -async function readGitHubFile(config, path) { - const response = await fetch(githubContentsUrl(config, path), { - headers: { - Accept: "application/vnd.github+json", - Authorization: `Bearer ${config.token}`, - "X-GitHub-Api-Version": "2022-11-28", - }, - }) - if (!response.ok) { - throw new Error(`GitHub path read failed for ${path}: ${response.status}`) - } - - const payload = await response.json() - if (Array.isArray(payload) || payload.type !== "file" || typeof payload.content !== "string") { - throw new Error(`GitHub path is not a file: ${path}`) - } - return Buffer.from(payload.content, "base64").toString("utf8") -} - -function githubContentsUrl(config, path) { - const encodedPath = path - .split("/") - .map((part) => encodeURIComponent(part)) - .join("/") - const ref = encodeURIComponent(config.branch) - return `https://api.github.com/repos/${config.owner}/${config.repo}/contents/${encodedPath}?ref=${ref}` -} - async function readRowsFromGitHub(selectRows, expectedCount) { for (let attempt = 0; attempt < 8; attempt += 1) { const rows = await selectRows() diff --git a/examples/api-plaintext/index.mjs b/examples/api-plaintext/index.mjs index fa8c843..0bf8920 100644 --- a/examples/api-plaintext/index.mjs +++ b/examples/api-plaintext/index.mjs @@ -32,8 +32,6 @@ await people.upsertMany([ { id: "p3", name: "Grace", team: "storage" }, ]) -const remoteManifestPath = `${config.prefix}/manifest.json` -await readGitHubFile(config, remoteManifestPath) const rows = await readRowsFromGitHub(async () => { const verificationDb = await GitDb.open({ store: new GitHubFetchPlaintextStore(config), @@ -47,7 +45,6 @@ const summary = { example: "api-plaintext", mode: "github-plaintext", prefix: config.prefix, - remoteManifestPath, remoteVerified: true, repo: `${config.owner}/${config.repo}`, rows, @@ -74,34 +71,6 @@ function requiredEnv(name, fallback) { return value } -async function readGitHubFile(config, path) { - const response = await fetch(githubContentsUrl(config, path), { - headers: { - Accept: "application/vnd.github+json", - Authorization: `Bearer ${config.token}`, - "X-GitHub-Api-Version": "2022-11-28", - }, - }) - if (!response.ok) { - throw new Error(`GitHub path read failed for ${path}: ${response.status}`) - } - - const payload = await response.json() - if (Array.isArray(payload) || payload.type !== "file" || typeof payload.content !== "string") { - throw new Error(`GitHub path is not a file: ${path}`) - } - return Buffer.from(payload.content, "base64").toString("utf8") -} - -function githubContentsUrl(config, path) { - const encodedPath = path - .split("/") - .map((part) => encodeURIComponent(part)) - .join("/") - const ref = encodeURIComponent(config.branch) - return `https://api.github.com/repos/${config.owner}/${config.repo}/contents/${encodedPath}?ref=${ref}` -} - async function readRowsFromGitHub(selectRows, expectedCount) { for (let attempt = 0; attempt < 8; attempt += 1) { const rows = await selectRows() diff --git a/site/app.js b/site/app.js index 1aa6219..5436bc6 100644 --- a/site/app.js +++ b/site/app.js @@ -1,118 +1,95 @@ -import { benchmarkLabels, benchmarkModes, content, languages, operationLabels } from "./content.js" - -const DEFAULT_LANGUAGE = "en" -const STORAGE_KEY = "gitdb-language" - -let activeLanguage = resolveInitialLanguage() -let benchmarkData = { - comparisons: [], - current: { metadata: null }, - scenarios: [], -} - -bindLanguageSwitch() -applyLanguage() -benchmarkData = await loadBenchmark() -render(benchmarkData) +const scenarioLabels = { + compaction: "로그 정리", + "github-encrypted": "GitHub 암호화 저장", + "github-plaintext": "GitHub 비암호화 저장", + "local-encrypted": "로컬 암호화 저장", + "local-plaintext": "로컬 비암호화 저장", + reopen: "다시 열기", + "table-indexed-lookup": "index 조회", + "table-insert": "insert single", + "table-insert-many": "insertMany batch", + "table-upsert-many": "upsertMany batch", + "table-upsert-single": "upsert single", +} + +const modeLabels = { + encrypted: "암호화", + "github encrypted": "GitHub 암호화", + "github plaintext": "GitHub 비암호화", + plaintext: "비암호화", +} + +const operationLabels = { + "encrypted-delete-filter": "한 팀의 데이터 삭제", + "encrypted-insert-batch": "한 번에 데이터 넣기", + "encrypted-join": "사람과 팀 join", + "encrypted-select-filter": "한 팀의 데이터 조회", + "github encrypted-delete-filter": "GitHub에서 한 팀의 데이터 삭제", + "github encrypted-insert-batch": "GitHub에 데이터 넣기", + "github encrypted-join": "GitHub에서 읽은 뒤 join", + "github encrypted-select-filter": "GitHub에서 한 팀의 데이터 조회", + "github plaintext-delete-filter": "GitHub에서 한 팀의 데이터 삭제", + "github plaintext-insert-batch": "GitHub에 데이터 넣기", + "github plaintext-join": "GitHub에서 읽은 뒤 join", + "github plaintext-select-filter": "GitHub에서 한 팀의 데이터 조회", + "plaintext-delete-filter": "한 팀의 데이터 삭제", + "plaintext-insert-batch": "한 번에 데이터 넣기", + "plaintext-join": "사람과 팀 join", + "plaintext-select-filter": "한 팀의 데이터 조회", +} + +const benchmark = await loadBenchmark() +renderBenchmark(benchmark) async function loadBenchmark() { try { const response = await fetch("./benchmark.json", { cache: "no-store" }) if (!response.ok) { - return benchmarkData + return emptyBenchmark() } return await response.json() } catch (error) { - console.warn("Benchmark evidence unavailable", error) - return benchmarkData + console.warn("benchmark evidence unavailable", error) + return emptyBenchmark() } } -function bindLanguageSwitch() { - for (const button of document.querySelectorAll("[data-language-option]")) { - button.addEventListener("click", () => { - const language = button.getAttribute("data-language-option") - if (isLanguage(language)) { - setLanguage(language) - } - }) - } +function emptyBenchmark() { + return { current: { metadata: null }, operations: [], scenarios: [] } } -function setLanguage(language) { - activeLanguage = language - storeLanguage(language) - const url = new URL(window.location.href) - url.searchParams.set("lang", language) - window.history.replaceState(null, "", url) - applyLanguage() - render(benchmarkData) +function renderBenchmark(data) { + const scenarios = Array.isArray(data.scenarios) ? data.scenarios : [] + const operations = Array.isArray(data.operations) ? data.operations : [] + renderHeroMetrics(scenarios) + renderMetadata(data.current?.metadata) + renderOperations(operations) + renderScenarios(scenarios) + renderBars(scenarios) } -function applyLanguage() { - const pageContent = activeContent() - document.documentElement.lang = activeLanguage - document.title = pageContent.title - document.documentElement.style.setProperty( - "--matrix-boundary-label", - JSON.stringify(pageContent.matrixBoundaryLabel), - ) - document.documentElement.style.setProperty( - "--matrix-guarantee-label", - JSON.stringify(pageContent.matrixGuaranteeLabel), - ) - - const description = document.getElementById("meta-description") - if (description !== null) { - description.setAttribute("content", pageContent.description) - } - - for (const element of document.querySelectorAll("[data-i18n]")) { - const key = element.getAttribute("data-i18n") - const value = key === null ? undefined : pageContent.copy[key] - if (value !== undefined) { - element.textContent = value - } - } - - for (const element of document.querySelectorAll("[data-i18n-aria-label]")) { - const key = element.getAttribute("data-i18n-aria-label") - const value = key === null ? undefined : pageContent.copy[key] - if (value !== undefined) { - element.setAttribute("aria-label", value) - } - } - - for (const button of document.querySelectorAll("[data-language-option]")) { - button.setAttribute( - "aria-pressed", - String(button.getAttribute("data-language-option") === activeLanguage), - ) - } +function renderHeroMetrics(scenarios) { + const github = findScenario(scenarios, "github-plaintext") + const upsert = findScenario(scenarios, "table-upsert-single") + const batch = findScenario(scenarios, "table-insert-many") + const indexed = findScenario(scenarios, "table-indexed-lookup") + setText("hero-github-writes", formatNumber(github?.writesPerSecond)) + setText("hero-bulk-ratio", formatRatio(batch?.writesPerSecond, upsert?.writesPerSecond)) + setText("hero-index-ms", formatMs(indexed?.joinMs)) } -function render(data) { - const scenarios = data.scenarios ?? [] - const comparisons = data.comparisons ?? [] - const local = findScenario(scenarios, "local-plaintext") - const githubPlaintext = findScenario(scenarios, "github-plaintext") - const upsertSingle = findScenario(scenarios, "table-upsert-single") - const insertMany = findScenario(scenarios, "table-insert-many") - const indexed = findScenario(scenarios, "table-indexed-lookup") - setText("hero-raw-wps", formatNumber(githubPlaintext?.writesPerSecond ?? local?.writesPerSecond)) - setText( - "hero-bulk-delta", - formatRatio(insertMany?.writesPerSecond, upsertSingle?.writesPerSecond), - ) +function renderMetadata(metadata) { + if (metadata === null || metadata === undefined) { + setText("benchmark-meta", "벤치마크 데이터를 불러오지 못했습니다.") + return + } + const localRows = metadata.rows ?? "n/a" + const githubRows = metadata.githubRows ?? "n/a" + const runtime = [metadata.node, metadata.platform, metadata.arch].filter(Boolean).join(", ") setText( - "hero-index-ms", - indexed?.joinMs === undefined ? "n/a" : `${formatNumber(indexed.joinMs)} ms`, + "benchmark-meta", + `Local ${localRows} rows, live GitHub ${githubRows} rows, ${runtime}, 반복 ${metadata.repeatCount ?? "n/a"}.`, ) - setText("benchmark-meta", metadataText(data.current?.metadata)) - renderOperations(data.operations ?? []) - renderComparisons(comparisons, scenarios) - renderRows(scenarios) - renderBars(scenarios) } function renderOperations(operations) { @@ -122,200 +99,123 @@ function renderOperations(operations) { } target.replaceChildren() if (operations.length === 0) { - const row = document.createElement("tr") - row.append(tableCell(activeContent().benchmarkPending, "left")) - row.append(tableCell("n/a")) - row.append(tableCell("n/a")) - row.append(tableCell("n/a")) - row.append(tableCell("n/a")) - target.append(row) + target.append(emptyRow("작업별 벤치마크를 불러오는 중입니다.", 5)) return } - for (const item of operations) { + for (const operation of operations) { const row = document.createElement("tr") - row.append(tableCell(operationModeLabel(item), "left")) - row.append(tableCell(operationLabel(item), "left")) - row.append(tableCell(rowCountLabel(item.rows))) - row.append(tableCell(rowCountLabel(item.resultRows))) - row.append(tableCell(`${formatNumber(item.timeMs)} ms`)) + row.append(cell(modeLabels[operation.mode] ?? operation.mode, "left")) + row.append(cell(operationLabels[operation.key] ?? operation.operation, "left")) + row.append(cell(rowCount(operation.rows))) + row.append(cell(rowCount(operation.resultRows))) + row.append(cell(formatMs(operation.timeMs))) target.append(row) } } -function renderComparisons(comparisons, scenarios) { - const target = document.getElementById("comparison-grid") - if (target === null) { - return - } - target.replaceChildren() - for (const item of comparisons.slice(0, 3)) { - const node = document.createElement("article") - node.className = "comparison" - node.append(smallText(comparisonLabel(item, scenarios))) - node.append(strongText(formatPercent(item.writesPerSecondChangePct))) - node.append(smallText(previousBenchmarkText(item.baseline?.writesPerSecond))) - target.append(node) - } -} - -function renderRows(scenarios) { - const target = document.getElementById("benchmark-rows") +function renderScenarios(scenarios) { + const target = document.getElementById("scenario-rows") if (target === null) { return } target.replaceChildren() if (scenarios.length === 0) { - const row = document.createElement("tr") - row.append(tableCell(activeContent().benchmarkPending, "left")) - row.append(tableCell("n/a")) - row.append(tableCell("n/a")) - row.append(tableCell("n/a")) - row.append(tableCell("n/a")) - target.append(row) + target.append(emptyRow("시나리오별 벤치마크를 불러오는 중입니다.", 5)) return } - for (const item of scenarios) { + for (const scenario of scenarios) { const row = document.createElement("tr") - row.append(tableCell(scenarioLabel(item), "left")) - row.append(tableCell(formatNumber(item.writesPerSecond))) - row.append(tableCell(`${formatNumber(item.writeMs)} ms`)) - row.append(tableCell(`${formatNumber(item.joinMs)} ms`)) - row.append(tableCell(`${formatNumber(item.reopenMs)} ms`)) + row.append(cell(scenarioLabels[scenario.key] ?? scenario.label, "left")) + row.append(cell(`${formatNumber(scenario.writesPerSecond)} w/s`)) + row.append(cell(formatMs(scenario.writeMs))) + row.append(cell(formatMs(scenario.joinMs))) + row.append(cell(formatMs(scenario.reopenMs))) target.append(row) } } function renderBars(scenarios) { - const target = document.getElementById("chart-bars") + const target = document.getElementById("scenario-bars") if (target === null) { return } target.replaceChildren() const max = scenarios.reduce( - (value, scenario) => Math.max(value, scenario.writesPerSecond ?? 0), + (current, scenario) => Math.max(current, scenario.writesPerSecond), 0, ) if (max === 0) { - target.append(smallText(activeContent().benchmarkPending)) + target.append(textNode("처리량 데이터를 불러오는 중입니다.")) return } - for (const item of scenarios) { - target.append(barRow(item, max)) + for (const scenario of scenarios) { + const row = document.createElement("div") + const label = document.createElement("div") + const track = document.createElement("div") + const bar = document.createElement("div") + label.className = "bar-label" + track.className = "bar-track" + bar.className = `bar ${barTone(scenario.key)}` + bar.style.width = `${Math.max(4, (scenario.writesPerSecond / max) * 100)}%` + label.append(textNode(scenarioLabels[scenario.key] ?? scenario.label)) + label.append(textNode(`${formatNumber(scenario.writesPerSecond)} w/s`)) + track.append(bar) + row.append(label) + row.append(track) + target.append(row) } } -function barRow(item, max) { - const row = document.createElement("div") - row.className = "bar-row" - - const label = document.createElement("div") - label.className = "bar-label" - label.append(smallText(scenarioLabel(item))) - label.append(smallText(`${formatNumber(item.writesPerSecond)} w/s`)) - - const track = document.createElement("div") - track.className = "bar-track" - track.append(bar(item.key, item.writesPerSecond, max)) - - row.append(label) - row.append(track) - return row -} - -function bar(key, value, max) { - const element = document.createElement("div") - element.className = `bar ${barTone(key)}` - element.style.width = `${Math.max(4, (value / max) * 100)}%` - return element -} - function barTone(key) { - if (key === "table-insert-many" || key === "table-indexed-lookup") { - return "accent" + if (key === "compaction" || key === "table-indexed-lookup") { + return "fast" } - if (key === "table-upsert-single" || key === "table-insert") { + if (key === "github-plaintext" || key === "github-encrypted" || key === "table-upsert-single") { return "slow" } - return "current" + return "" } -function tableCell(value, align = "right") { - const cell = document.createElement("td") - cell.textContent = value - if (align === "left") { - cell.style.textAlign = "left" - } - return cell +function findScenario(scenarios, key) { + return scenarios.find((scenario) => scenario.key === key) } -function smallText(value) { - const element = document.createElement("span") +function cell(value, align = "right") { + const element = document.createElement("td") element.textContent = value + if (align === "left") { + element.style.textAlign = "left" + } return element } -function strongText(value) { - const element = document.createElement("strong") +function emptyRow(message, span) { + const row = document.createElement("tr") + const element = cell(message, "left") + element.colSpan = span + row.append(element) + return row +} + +function textNode(value) { + const element = document.createElement("span") element.textContent = value return element } function setText(id, value) { - const element = document.getElementById(id) - if (element !== null) { - element.textContent = value - } -} - -function findScenario(scenarios, key) { - return scenarios.find((scenario) => scenario.key === key) -} - -function comparisonLabel(item, scenarios) { - const scenario = scenarios.find((candidate) => candidate.label === item.scenario) - return scenario === undefined ? item.scenario : scenarioLabel(scenario) -} - -function scenarioLabel(item) { - return benchmarkLabels[activeLanguage]?.[item.key] ?? item.label -} - -function operationModeLabel(item) { - return benchmarkModes[activeLanguage]?.[item.mode] ?? item.mode -} - -function operationLabel(item) { - return operationLabels[activeLanguage]?.[item.key] ?? item.operation -} - -function rowCountLabel(value) { - return typeof value === "number" ? formatTemplate(activeContent().rowCount, { value }) : "n/a" -} - -function metadataText(metadata) { - if (metadata === null || metadata === undefined) { - return activeContent().benchmarkUnavailable + const target = document.getElementById(id) + if (target !== null) { + target.textContent = value } - const template = - metadata.githubNetwork === true - ? activeContent().benchmarkMetaGithub - : activeContent().benchmarkMeta - return formatTemplate(template, metadata) } -function previousBenchmarkText(value) { - return formatTemplate(activeContent().previousBenchmark, { - value: formatNumber(value), - }) +function rowCount(value) { + return typeof value === "number" ? `${formatNumber(value)} rows` : "n/a" } -function formatNumber(value) { - return typeof value === "number" - ? new Intl.NumberFormat(activeContent().locale, { - maximumFractionDigits: 2, - minimumFractionDigits: 0, - }).format(value) - : "n/a" +function formatMs(value) { + return typeof value === "number" ? `${formatNumber(value)} ms` : "n/a" } function formatRatio(after, before) { @@ -324,46 +224,8 @@ function formatRatio(after, before) { : "n/a" } -function formatPercent(value) { - return typeof value === "number" ? `${value >= 0 ? "+" : ""}${formatNumber(value)}%` : "n/a" -} - -function formatTemplate(template, values) { - return template.replace(/\{([a-zA-Z]+)\}/g, (_match, key) => String(values[key] ?? "n/a")) -} - -function activeContent() { - return content[activeLanguage] -} - -function resolveInitialLanguage() { - const urlLanguage = new URLSearchParams(window.location.search).get("lang") - if (isLanguage(urlLanguage)) { - return urlLanguage - } - const stored = storedLanguage() - if (isLanguage(stored)) { - return stored - } - return navigator.language.startsWith("ko") ? "ko" : DEFAULT_LANGUAGE -} - -function isLanguage(value) { - return typeof value === "string" && languages.includes(value) -} - -function storedLanguage() { - try { - return window.localStorage.getItem(STORAGE_KEY) - } catch { - return null - } -} - -function storeLanguage(language) { - try { - window.localStorage.setItem(STORAGE_KEY, language) - } catch { - return - } +function formatNumber(value) { + return typeof value === "number" + ? new Intl.NumberFormat("ko-KR", { maximumFractionDigits: 2 }).format(value) + : "n/a" } diff --git a/site/benchmark.json b/site/benchmark.json index be5a18c..654628a 100644 --- a/site/benchmark.json +++ b/site/benchmark.json @@ -89,11 +89,11 @@ "arch": "arm64", "cpu": "Apple M5", "generatedAt": "2026-06-15T05:30:37.126Z", + "githubNetwork": true, + "githubRows": 25, "node": "v24.11.0", "platform": "darwin", "release": "25.5.0", - "githubNetwork": true, - "githubRows": 25, "repeatCount": 1, "rows": 250, "warmupCount": 0 @@ -235,102 +235,102 @@ "scenarios": [ { "joinMs": 2.4502080000002024, + "key": "local-plaintext", "label": "local plaintext transaction batch", "reopenMs": 32.184625000000096, "rows": 250, "writeMs": 1052.0475419999998, - "writesPerSecond": 237.63184648931013, - "key": "local-plaintext" + "writesPerSecond": 237.63184648931013 }, { "joinMs": 0.7159579999997732, + "key": "local-encrypted", "label": "local encrypted transaction batch", "reopenMs": 38.051750000000084, "rows": 250, "writeMs": 2049.4547920000005, - "writesPerSecond": 121.98366169181641, - "key": "local-encrypted" + "writesPerSecond": 121.98366169181641 }, { "joinMs": 0.7292919999999867, + "key": "table-upsert-single", "label": "table upsert() single transactions", "reopenMs": 87.54754099999991, "rows": 250, "writeMs": 9382.456458, - "writesPerSecond": 26.6454740418045, - "key": "table-upsert-single" + "writesPerSecond": 26.6454740418045 }, { "joinMs": 0.17374999999810825, + "key": "table-insert", "label": "table insert() single transactions", "reopenMs": 47.94787500000166, "rows": 250, "writeMs": 8189.760916000001, - "writesPerSecond": 30.52592164340051, - "key": "table-insert" + "writesPerSecond": 30.52592164340051 }, { "joinMs": 0.058625000001484295, + "key": "table-insert-many", "label": "table insertMany bulk transaction", "reopenMs": 41.13616699999693, "rows": 250, "writeMs": 1172.6108749999985, - "writesPerSecond": 213.19945544595117, - "key": "table-insert-many" + "writesPerSecond": 213.19945544595117 }, { "joinMs": 0.16341700000339188, + "key": "table-upsert-many", "label": "table upsertMany bulk transaction", "reopenMs": 86.96625000000131, "rows": 250, "writeMs": 2216.9628749999974, - "writesPerSecond": 112.76688609411211, - "key": "table-upsert-many" + "writesPerSecond": 112.76688609411211 }, { "joinMs": 0.17725000000064028, + "key": "table-indexed-lookup", "label": "table indexed equality lookup", "reopenMs": 65.94270899999901, "rows": 250, "writeMs": 1135.0013340000005, - "writesPerSecond": 220.26405829757368, - "key": "table-indexed-lookup" + "writesPerSecond": 220.26405829757368 }, { "joinMs": 2.8039999999964493, + "key": "reopen", "label": "local plaintext reopen checkpoint", "reopenMs": 52.7922499999986, "rows": 250, "writeMs": 986.5166250000002, - "writesPerSecond": 253.41691530033768, - "key": "reopen" + "writesPerSecond": 253.41691530033768 }, { "joinMs": 0.639500000001135, + "key": "compaction", "label": "local plaintext compaction", "reopenMs": 20.485375000000204, "rows": 250, "writeMs": 95.06695799999943, - "writesPerSecond": 2629.72546150053, - "key": "compaction" + "writesPerSecond": 2629.72546150053 }, { "joinMs": 1.527458000004117, + "key": "github-plaintext", "label": "github plaintext tree commits (gitdb/live-benchmark-20260615T052802Z/scenario-plaintext)", "reopenMs": 3075.2933330000014, "rows": 25, "writeMs": 5204.182916999998, - "writesPerSecond": 4.803828074208332, - "key": "github-plaintext" + "writesPerSecond": 4.803828074208332 }, { "joinMs": 2.045916999995825, + "key": "github-encrypted", "label": "github encrypted tree commits (gitdb/live-benchmark-20260615T052802Z/scenario-encrypted)", "reopenMs": 11653.092917000009, "rows": 25, "writeMs": 2658.973665999998, - "writesPerSecond": 9.402123954694337, - "key": "github-encrypted" + "writesPerSecond": 9.402123954694337 } ] } diff --git a/site/benchmark.md b/site/benchmark.md index 4dd8f90..166fa69 100644 --- a/site/benchmark.md +++ b/site/benchmark.md @@ -1,26 +1,6 @@ -| Scenario | Previous writes/s | Current writes/s | Change | Write ms change | Join ms change | -| --- | ---: | ---: | ---: | ---: | ---: | -| local plaintext transaction batch | 173.14 | 237.63 | +37.25% | +27.14% | +92.57% | -| local encrypted transaction batch | 328.09 | 121.98 | -62.82% | -168.96% | +83.39% | -| table indexed equality lookup | 46.49 | 220.26 | +373.79% | +78.89% | +86.37% | +# GitDB Website Benchmark Evidence -| Mode | Operation | Data rows | Rows returned/changed | Time ms | -| --- | --- | ---: | ---: | ---: | -| github plaintext | insert rows to GitHub | 25 | 25 | 4819.20 | -| github plaintext | select one team's rows from GitHub | 25 | 3 | 2502.74 | -| github plaintext | join rows after GitHub read | 25 | 25 | 2598.04 | -| github plaintext | delete one team's rows on GitHub | 25 | 3 | 7359.05 | -| github encrypted | insert rows to GitHub | 25 | 25 | 2478.51 | -| github encrypted | select one team's rows from GitHub | 25 | 3 | 12698.05 | -| github encrypted | join rows after GitHub read | 25 | 25 | 12338.65 | -| github encrypted | delete one team's rows on GitHub | 25 | 3 | 14208.38 | -| plaintext | insert rows in one transaction | 250 | 250 | 1048.42 | -| plaintext | select one team's rows | 250 | 25 | 0.63 | -| plaintext | join people to teams | 250 | 250 | 1.87 | -| plaintext | delete one team's rows | 250 | 25 | 26.65 | -| encrypted | insert rows in one transaction | 250 | 250 | 2398.01 | -| encrypted | select one team's rows | 250 | 25 | 0.48 | -| encrypted | join people to teams | 250 | 250 | 0.72 | -| encrypted | delete one team's rows | 250 | 25 | 21.54 | +This static site renders the benchmark evidence stored in `site/benchmark.json`. -Runtime: v24.11.0 on darwin/arm64; repeat=1, warmup=0 +Source run: `GITDB_BENCH_ROWS=250 GITDB_BENCH_GITHUB_ROWS=25 corepack pnpm benchmark:compare:github` +captured on 2026-06-15 KST and documented in `docs/BENCHMARKS.md`. diff --git a/site/content.js b/site/content.js deleted file mode 100644 index 1038611..0000000 --- a/site/content.js +++ /dev/null @@ -1,289 +0,0 @@ -export const languages = ["en", "ko"] - -export const content = { - en: { - locale: "en-US", - title: "GitDB - GitHub as an application database", - description: - "GitDB is a serverless RDB-like database that stores application data in a GitHub repository.", - matrixBoundaryLabel: "Boundary", - matrixGuaranteeLabel: "Guarantee", - benchmarkMeta: - "{rows} rows, {node}, {platform}/{arch}, repeat {repeatCount}, warmup {warmupCount}.", - benchmarkMetaGithub: - "Local {rows} rows; live GitHub {githubRows} rows, {node}, {platform}/{arch}, repeat {repeatCount}.", - benchmarkUnavailable: "Benchmark metadata unavailable.", - benchmarkPending: "Benchmark data is still loading.", - previousBenchmark: "previous {value} w/s", - rowCount: "{value} rows", - copy: { - "actions.install": "npm install", - "actions.source": "View source", - "benchmark.axisFaster": "faster", - "benchmark.axisSlower": "slower", - "benchmark.body": - "The table includes local baseline calls and live GitHub calls that really insert, read, join, and delete rows through the network. GitHub rows include API latency and Git commit time.", - "benchmark.dataRows": "Data set", - "benchmark.eyebrow": "Operation timings", - "benchmark.heading": "What common database calls cost with real data.", - "benchmark.mode": "Mode", - "benchmark.operation": "Operation", - "benchmark.reopen": "Open again", - "benchmark.resultRows": "Rows returned/changed", - "benchmark.scenario": "Scenario", - "benchmark.query": "Lookup/join", - "benchmark.time": "Time", - "benchmark.writes": "Writes/s", - "benchmark.writeMs": "Write time", - "footer.architecture": "Architecture", - "footer.benchmarks": "Benchmarks", - "footer.release": "Release", - "guarantees.boundary": "Boundary", - "guarantees.capability": "Capability", - "guarantees.current": "Current guarantee", - "guarantees.eyebrow": "Guarantee matrix", - "guarantees.heading": "GitHub is the durable database store.", - "guarantees.intro": - "GitDB stores database state in a GitHub repository. The manifest and mutation logs are the source of truth; derived indexes and snapshots can be rebuilt.", - "guarantees.row.compaction.boundary": "Local plaintext store", - "guarantees.row.compaction.capability": "Compaction", - "guarantees.row.compaction.current": - "Checkpoint first, manifest advance second, obsolete log deletion last.", - "guarantees.row.concurrency.boundary": "Not distributed multi-writer OLTP", - "guarantees.row.concurrency.capability": "Concurrency", - "guarantees.row.concurrency.current": - "One writer per opened database, with stale lock recovery and multiple readers.", - "guarantees.row.durability.boundary": "Manifest sequence and committed log segments", - "guarantees.row.durability.capability": "Durability", - "guarantees.row.durability.current": - "Manifest-gated replay with checkpoint validation on reopen.", - "guarantees.row.github.boundary": "GitHub latency and rate limits apply", - "guarantees.row.github.capability": "GitHub database", - "guarantees.row.github.current": "Writes use Git tree commits and non-force ref updates.", - "guarantees.row.indexes.boundary": "Derived data for safe equality lookups", - "guarantees.row.indexes.capability": "Indexes", - "guarantees.row.indexes.current": - "Equality indexes rebuild from committed rows and are ignored when stale.", - "guarantees.row.snapshots.boundary": "Optimization; logs remain authoritative", - "guarantees.row.snapshots.capability": "Snapshots", - "guarantees.row.snapshots.current": "Plaintext page snapshots accelerate reopen and review.", - "guarantees.row.transactions.boundary": "In-process queue plus store commit boundary", - "guarantees.row.transactions.capability": "Transactions", - "guarantees.row.transactions.current": - "Serialized mutations become visible only after manifest advance.", - "hero.eyebrow": "Serverless RDB on GitHub", - "hero.lede": - "GitDB lets a serverless app use a GitHub repository like a small relational database: tables, SQL-style reads, transactions, optional encryption, and real Git commits.", - "hero.metric.bulk": "Bulk vs single", - "hero.metric.index": "Indexed select", - "hero.metric.write": "GitHub writes/s", - "hero.proof.api.body": - "Use tables, indexed selects, joins, deletes, and transactions over repository-backed state.", - "hero.proof.api.title": "RDB-like behavior", - "hero.proof.audit.body": - "Plaintext and encrypted data are persisted through Git tree commits.", - "hero.proof.audit.title": "Real GitHub commits", - "hero.proof.local.body": - "Serverless apps write to GitHub directly; there is no hosted database process to run.", - "hero.proof.local.title": "No database server", - "hero.repo.commit": "each write becomes a Git commit", - "hero.repo.label": "github repo", - "language.label": "Language", - "nav.benchmarks": "Benchmarks", - "nav.github": "GitHub", - "nav.guarantees": "Guarantees", - "nav.serverless": "Serverless", - "serverless.eyebrow": "Serverless database", - "serverless.heading": "Use GitHub as the database, not just file storage.", - "serverless.intro": - "GitDB is for apps that cannot or should not run a database server, but still need relational database habits: schema, rows, queries, transactions, and history.", - "serverless.step.app.body": - "Extensions, static apps, agents, and tools keep their data in a GitHub repo.", - "serverless.step.app.title": "App runs serverless", - "serverless.step.github.body": - "Manifest and log files are written to the repo, with Git history as the audit trail.", - "serverless.step.github.title": "GitHub stores the database", - "serverless.step.rdb.body": - "Rows, indexes, joins, deletes, and transactions are exposed as database operations.", - "serverless.step.rdb.title": "GitDB behaves like a small RDB", - }, - }, - ko: { - locale: "ko-KR", - title: "GitDB - GitHub를 애플리케이션 데이터베이스로", - description: "GitDB는 서버리스 앱이 GitHub 저장소를 RDB처럼 쓰게 해주는 데이터베이스입니다.", - matrixBoundaryLabel: "경계", - matrixGuaranteeLabel: "보장", - benchmarkMeta: - "{rows} rows, {node}, {platform}/{arch}, 반복 {repeatCount}, 워밍업 {warmupCount}.", - benchmarkMetaGithub: - "Local {rows} rows, live GitHub {githubRows} rows, {node}, {platform}/{arch}, 반복 {repeatCount}.", - benchmarkUnavailable: "벤치마크 정보를 불러오지 못했습니다.", - benchmarkPending: "벤치마크 데이터를 불러오는 중입니다.", - previousBenchmark: "이전 기준 {value} w/s", - rowCount: "{value} rows", - copy: { - "actions.install": "npm 설치", - "actions.source": "소스 보기", - "benchmark.axisFaster": "빠름", - "benchmark.axisSlower": "느림", - "benchmark.body": - "표에는 local 기준값과 실제 GitHub에 row를 넣고, 읽고, join하고, 지운 네트워크 결과가 함께 들어갑니다. GitHub 행에는 API 왕복과 Git commit 시간이 포함됩니다.", - "benchmark.dataRows": "데이터셋", - "benchmark.eyebrow": "작업별 걸린 시간", - "benchmark.heading": "실제 데이터로 자주 쓰는 DB 작업 시간을 보여줍니다.", - "benchmark.mode": "모드", - "benchmark.operation": "작업", - "benchmark.reopen": "다시 열기", - "benchmark.resultRows": "반환/변경 row", - "benchmark.scenario": "시나리오", - "benchmark.query": "조회/join", - "benchmark.time": "시간", - "benchmark.writes": "Writes/s", - "benchmark.writeMs": "Write 시간", - "footer.architecture": "아키텍처", - "footer.benchmarks": "벤치마크", - "footer.release": "릴리스", - "guarantees.boundary": "경계", - "guarantees.capability": "기능", - "guarantees.current": "현재 보장", - "guarantees.eyebrow": "보장 매트릭스", - "guarantees.heading": "GitHub가 durable database store입니다.", - "guarantees.intro": - "GitDB는 database state를 GitHub repository에 저장합니다. Manifest와 mutation log가 원본이고, index와 snapshot은 다시 만들 수 있습니다.", - "guarantees.row.compaction.boundary": "로컬 plaintext store", - "guarantees.row.compaction.capability": "Compaction", - "guarantees.row.compaction.current": - "먼저 checkpoint를 쓰고, 다음에 manifest를 전진시키고, 마지막에 오래된 로그를 지웁니다.", - "guarantees.row.concurrency.boundary": "분산 multi-writer OLTP가 아닙니다", - "guarantees.row.concurrency.capability": "동시성", - "guarantees.row.concurrency.current": - "열린 database마다 writer 하나를 두고 stale lock recovery와 multiple reader를 지원합니다.", - "guarantees.row.durability.boundary": "Manifest sequence와 커밋된 log segment", - "guarantees.row.durability.capability": "내구성", - "guarantees.row.durability.current": - "Reopen 시 checkpoint 검증과 manifest 기반 replay로 복구합니다.", - "guarantees.row.github.boundary": "GitHub latency와 rate limit이 적용됩니다", - "guarantees.row.github.capability": "GitHub database", - "guarantees.row.github.current": - "Write는 Git tree commit과 non-force ref update를 사용합니다.", - "guarantees.row.indexes.boundary": "안전한 equality lookup을 위한 파생 데이터", - "guarantees.row.indexes.capability": "Index", - "guarantees.row.indexes.current": - "Equality index는 커밋된 row에서 다시 만들 수 있고 stale이면 무시됩니다.", - "guarantees.row.snapshots.boundary": "최적화이며 로그가 권위 있는 원본입니다", - "guarantees.row.snapshots.capability": "Snapshot", - "guarantees.row.snapshots.current": - "Plaintext page snapshot으로 reopen과 review를 빠르게 합니다.", - "guarantees.row.transactions.boundary": "In-process queue와 store commit boundary", - "guarantees.row.transactions.capability": "Transaction", - "guarantees.row.transactions.current": - "직렬화된 mutation은 manifest가 전진한 뒤에만 보입니다.", - "hero.eyebrow": "GitHub 기반 서버리스 RDB", - "hero.lede": - "GitDB는 서버리스 앱이 GitHub 저장소를 작은 관계형 데이터베이스처럼 쓰게 해줍니다. Table, SQL식 조회, transaction, 선택적 암호화, 실제 Git commit을 제공합니다.", - "hero.metric.bulk": "Bulk와 single", - "hero.metric.index": "Indexed select", - "hero.metric.write": "GitHub writes/s", - "hero.proof.api.body": - "Repository-backed state 위에서 table, indexed select, join, delete, transaction을 사용합니다.", - "hero.proof.api.title": "RDB처럼 동작", - "hero.proof.audit.body": "Plaintext/encrypted data가 Git tree commit으로 저장됩니다.", - "hero.proof.audit.title": "실제 GitHub commit", - "hero.proof.local.body": - "서버리스 앱이 GitHub에 직접 write합니다. 따로 띄우는 database process가 없습니다.", - "hero.proof.local.title": "DB 서버 없음", - "hero.repo.commit": "write마다 Git commit이 됩니다", - "hero.repo.label": "github repo", - "language.label": "언어", - "nav.benchmarks": "벤치마크", - "nav.github": "GitHub", - "nav.guarantees": "보장", - "nav.serverless": "서버리스", - "serverless.eyebrow": "서버리스 데이터베이스", - "serverless.heading": "GitHub를 파일 저장소가 아니라 데이터베이스로 씁니다.", - "serverless.intro": - "GitDB는 DB 서버를 둘 수 없거나 두고 싶지 않은 앱을 위한 것입니다. 그래도 schema, row, query, transaction, history 같은 RDB식 습관은 유지합니다.", - "serverless.step.app.body": - "Extension, static app, agent, tool이 데이터를 GitHub repo에 둡니다.", - "serverless.step.app.title": "앱은 서버리스로 실행", - "serverless.step.github.body": - "Manifest와 log 파일이 repo에 쓰이고, Git history가 audit trail이 됩니다.", - "serverless.step.github.title": "GitHub가 데이터베이스 저장소", - "serverless.step.rdb.body": - "Row, index, join, delete, transaction을 database operation으로 사용합니다.", - "serverless.step.rdb.title": "GitDB는 작은 RDB처럼 동작", - }, - }, -} - -export const benchmarkLabels = { - en: {}, - ko: { - compaction: "로컬 plaintext compaction", - "github-encrypted": "GitHub encrypted tree commits", - "github-plaintext": "GitHub plaintext tree commits", - "local-encrypted": "로컬 encrypted transaction batch", - "local-plaintext": "로컬 plaintext transaction batch", - reopen: "로컬 plaintext reopen checkpoint", - "table-indexed-lookup": "테이블 indexed equality lookup", - "table-insert": "테이블 insert() single transaction", - "table-insert-many": "테이블 insertMany bulk transaction", - "table-upsert-many": "테이블 upsertMany bulk transaction", - "table-upsert-single": "테이블 upsert() single transaction", - }, -} - -export const benchmarkModes = { - en: { - encrypted: "Encrypted", - "github encrypted": "GitHub encrypted", - "github plaintext": "GitHub plaintext", - plaintext: "Plaintext", - }, - ko: { - encrypted: "암호화", - "github encrypted": "GitHub 암호화", - "github plaintext": "GitHub 비암호화", - plaintext: "비암호화", - }, -} - -export const operationLabels = { - en: { - "encrypted-delete-filter": "Delete one team's rows", - "encrypted-insert-batch": "Insert rows in one transaction", - "encrypted-join": "Join people to teams", - "encrypted-select-filter": "Select one team's rows", - "github encrypted-delete-filter": "Delete one team's rows on GitHub", - "github encrypted-insert-batch": "Insert rows to GitHub", - "github encrypted-join": "Join rows after GitHub read", - "github encrypted-select-filter": "Select one team's rows from GitHub", - "github plaintext-delete-filter": "Delete one team's rows on GitHub", - "github plaintext-insert-batch": "Insert rows to GitHub", - "github plaintext-join": "Join rows after GitHub read", - "github plaintext-select-filter": "Select one team's rows from GitHub", - "plaintext-delete-filter": "Delete one team's rows", - "plaintext-insert-batch": "Insert rows in one transaction", - "plaintext-join": "Join people to teams", - "plaintext-select-filter": "Select one team's rows", - }, - ko: { - "encrypted-delete-filter": "한 팀의 데이터 삭제", - "encrypted-insert-batch": "한 번에 데이터 넣기", - "encrypted-join": "사람과 팀 데이터 join", - "encrypted-select-filter": "한 팀의 데이터 조회", - "github encrypted-delete-filter": "GitHub에서 한 팀의 데이터 삭제", - "github encrypted-insert-batch": "GitHub에 데이터 넣기", - "github encrypted-join": "GitHub에서 읽은 뒤 join", - "github encrypted-select-filter": "GitHub에서 한 팀의 데이터 조회", - "github plaintext-delete-filter": "GitHub에서 한 팀의 데이터 삭제", - "github plaintext-insert-batch": "GitHub에 데이터 넣기", - "github plaintext-join": "GitHub에서 읽은 뒤 join", - "github plaintext-select-filter": "GitHub에서 한 팀의 데이터 조회", - "plaintext-delete-filter": "한 팀의 데이터 삭제", - "plaintext-insert-batch": "한 번에 데이터 넣기", - "plaintext-join": "사람과 팀 데이터 join", - "plaintext-select-filter": "한 팀의 데이터 조회", - }, -} diff --git a/site/favicon.svg b/site/favicon.svg index f9dbfec..9c532d3 100644 --- a/site/favicon.svg +++ b/site/favicon.svg @@ -1,7 +1,7 @@ - - - - - - + + + diff --git a/site/index.html b/site/index.html index 8571d59..c60a29e 100644 --- a/site/index.html +++ b/site/index.html @@ -1,247 +1,208 @@ - + - GitDB - GitHub as an application database + GitDB - GitHub 기반 서버리스 RDB
- + G GitDB -
-
-
-

Serverless RDB on GitHub

-

GitDB

-

- GitDB lets a serverless app use a GitHub repository like a small relational database: - tables, SQL-style reads, transactions, optional encryption, and real Git commits. -

- -
    -
  • - No database server - - Serverless apps write to GitHub directly; there is no hosted database process to run. - -
  • -
  • - RDB-like behavior - - Use tables, indexed selects, joins, deletes, and transactions over repository-backed state. - -
  • -
  • - Real GitHub commits - - Plaintext and encrypted data are persisted through Git tree commits. - -
  • -
-
-
-
Plaintext writes/s
-
loading
-
-
-
Bulk vs single
-
loading
-
-
-
Indexed select
-
loading
-
-
-
-
-
- github repo - my-app-db -
    -
  • gitdb/v1/manifest.json
  • -
  • gitdb/v1/log/0000000001.json
  • -
  • gitdb/v1/log/0000000002.json
  • -
  • gitdb/v1/snapshots/people.json
  • -
-

each write becomes a Git commit

-
+
+

GitHub 기반 서버리스 RDB

+

+ DB 서버 없이, + GitHub repo를 + 데이터베이스로. +

+

+ GitDB는 extension, static app, agent, 작은 tool이 table, SQL식 조회, + transaction, index, 선택적 암호화를 GitHub commit 위에서 쓰게 해줍니다. +

+
+ + +
+ +
+
+ 01 +

데이터가 있는 곳

+

사용자가 정한 GitHub 저장소입니다. Plaintext라면 파일을 그대로 review할 수 있습니다.

+
+
+ 02 +

앱이 얻는 것

+

Table, row, index, select, join, delete, transaction 같은 작은 RDB 동작입니다.

+
+
+ 03 +

write 비용

+

GitHub API 왕복과 Git commit 시간이 듭니다. hot OLTP보다 저빈도 앱 데이터에 맞습니다.

+
-
+
-

Serverless database

-

Use GitHub as the database, not just file storage.

-

- GitDB is for apps that cannot or should not run a database server, but still need - relational database habits: schema, rows, queries, transactions, and history. +

동작 방식

+

DB 호출을 GitHub repository commit으로 바꿉니다.

+

+ 앱은 GitDB API를 호출합니다. GitDB는 데이터베이스 상태를 GitHub에 씁니다. + 열릴 때는 커밋된 파일을 읽고 table 상태를 복구합니다.

-
+
- 01 -

App runs serverless

-

Extensions, static apps, agents, and tools keep their data in a GitHub repo.

+ API +

앱 호출

+

row를 넣고, index로 조회하고, table을 join하고, transaction으로 묶습니다.

- 02 -

GitDB behaves like a small RDB

-

Rows, indexes, joins, deletes, and transactions are exposed as database operations.

+ LOG +

데이터베이스 파일 작성

+

GitDB는 데이터베이스를 manifest, log, index, snapshot 파일로 직렬화합니다.

- 03 -

GitHub stores the database

-

Manifest and log files are written to the repo, with Git history as the audit trail.

+ GIT +

GitHub에 commit

+

저장은 Git tree 생성과 non-force ref update로 이루어집니다.

+
+
+ OPEN +

다시 열기

+

Manifest가 가리키는 log와 snapshot만 읽어 database state를 다시 만듭니다.

-
+
-

Guarantee matrix

-

GitHub is the durable database store.

-

- GitDB stores committed database state in a GitHub repository. The manifest and mutation - logs are the source of truth; derived indexes and snapshots can be rebuilt. -

+

선택지가 명확한 곳

+

느리게 보이는 것이 아니라, 일부러 느린 경계를 드러냅니다.

+

GitDB는 일반 SQL 서버를 흉내 낸 대체물이 아닙니다. GitHub를 데이터 저장소로 쓰는 작은 RDB runtime입니다.

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CapabilityCurrent guaranteeBoundary
TransactionsSerialized mutations become visible only after manifest advance.In-process queue plus store commit boundary
ConcurrencyLocal single writer with stale lock recovery and multiple readers.Not distributed multi-writer OLTP
DurabilityManifest-gated replay with checkpoint validation on reopen.Manifest sequence and committed log segments
SnapshotsPlaintext page snapshots accelerate reopen and review.Optimization; logs remain authoritative
IndexesEquality indexes rebuild from committed rows and are ignored when stale.Derived data for safe equality lookups
CompactionCheckpoint first, manifest advance second, obsolete log deletion last.Local plaintext store
GitHub databaseWrites use Git tree commits and non-force ref updates.GitHub latency and rate limits apply
+
+
+

맞는 경우

+
    +
  • Extension, demo, agent, internal app
  • +
  • Git history가 의미 있는 데이터
  • +
  • GitHub 네트워크 시간을 감수할 수 있는 앱
  • +
+
+
+

맞지 않는 경우

+
    +
  • 트래픽이 많은 production OLTP
  • +
  • 여러 writer가 동시에 쓰는 realtime 협업
  • +
  • 사용자 GitHub token으로 접근되면 안 되는 secret
  • +
+
-
+
-

Measured performance

-

What common database calls cost on this machine.

-

- Plaintext and encrypted runs use the same row count so the numbers are easy to compare. -

-

Loading benchmark metadata.

+

벤치마크

+

실제 데이터로 자주 쓰는 DB 작업 시간을 보여줍니다.

+

벤치마크 데이터를 불러오는 중입니다.

-
- + +
+
- - - - - + + + + +
ModeOperationData setRows returned/changedTime모드작업데이터셋반환/변경 row시간
-
-
-
-
- slower - faster + +
+
+
+ 느림 + 빠름
-
+
-
+ +
- - - - - + + + + + - +
ScenarioWrites/sWrite msQuery msReopen ms시나리오writes/swritequery/joinreopen
-
diff --git a/site/styles.css b/site/styles.css index 274addc..bcf6477 100644 --- a/site/styles.css +++ b/site/styles.css @@ -1,22 +1,18 @@ :root { - color: #111514; - background: #f6f7f2; - font-family: - "IBM Plex Sans", "Avenir Next", "Sohne", ui-sans-serif, system-ui, -apple-system, sans-serif; + --ink: #111615; + --muted: #57615d; + --paper: #f5f1e8; + --panel: #fffaf0; + --line: #d5cdbc; + --teal: #00745e; + --blue: #244f7a; + --red: #d2583b; + --wash: #dcebf0; + color: var(--ink); + background: var(--paper); + font-family: "Avenir Next", "IBM Plex Sans KR", "Apple SD Gothic Neo", "Segoe UI", sans-serif; font-synthesis: none; letter-spacing: 0; - text-rendering: optimizeLegibility; - --ink: #111514; - --muted: #5c6662; - --paper: #f6f7f2; - --panel: #ffffff; - --line: #cbd6d0; - --green: #0f6b5f; - --rust: #d85a3a; - --blue: #2d4f7c; - --gold: #bc902d; - --matrix-boundary-label: "Boundary"; - --matrix-guarantee-label: "Guarantee"; } * { @@ -25,8 +21,9 @@ body { margin: 0; - background: var(--paper); - color: var(--ink); + background: + linear-gradient(90deg, rgba(17, 22, 21, 0.05) 1px, transparent 1px) 0 0 / 48px 48px, + var(--paper); } a { @@ -34,11 +31,6 @@ a { text-decoration: none; } -code, -pre { - font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace; -} - .topbar { position: sticky; top: 0; @@ -46,32 +38,28 @@ pre { display: flex; align-items: center; justify-content: space-between; - min-height: 68px; - padding: 12px 28px; - border-bottom: 1px solid rgba(17, 21, 20, 0.16); - background: rgba(246, 247, 242, 0.94); - backdrop-filter: blur(16px); - box-shadow: 0 12px 36px rgba(17, 21, 20, 0.06); - gap: 22px; + min-height: 72px; + padding: 14px 30px; + border-bottom: 1px solid rgba(17, 22, 21, 0.18); + background: rgba(245, 241, 232, 0.94); + backdrop-filter: blur(18px); } .brand, -.nav, -.actions, -.language-switch, -.metric-strip, -.benchmark-layout, -.comparison-grid, -.serverless-grid, +nav, +.hero-actions, +.proof-strip, +.flow-grid, +.limits-grid, +.benchmark-lower, footer { display: flex; - align-items: center; } .brand { - gap: 10px; - font-size: 18px; - font-weight: 800; + align-items: center; + gap: 12px; + font-weight: 900; } .brand-mark { @@ -84,98 +72,34 @@ footer { color: var(--paper); } -.nav { - gap: 22px; - margin-left: auto; - color: var(--muted); - font-size: 14px; - font-weight: 680; -} - -.language-switch { - gap: 4px; - padding: 4px; - border: 1px solid rgba(17, 21, 20, 0.16); - border-radius: 8px; - background: rgba(255, 255, 255, 0.62); -} - -.language-switch button { - min-width: 58px; - min-height: 32px; - padding: 6px 10px; - border: 0; - border-radius: 6px; - background: transparent; - color: var(--muted); - cursor: pointer; - font: inherit; - font-size: 12px; - font-weight: 820; -} - -.language-switch button[aria-pressed="true"] { - background: var(--ink); - color: var(--paper); +nav { + gap: 24px; + color: #2f3734; + font-size: 15px; + font-weight: 800; } +nav a:hover, footer a:hover { - color: var(--green); -} - -.nav a:hover { - color: var(--green); -} - -main { - overflow: hidden; + color: var(--teal); } .hero { - position: relative; - min-height: min(740px, calc(100svh - 92px)); - padding: 74px 28px 58px; - overflow: hidden; - border-bottom: 1px solid rgba(17, 21, 20, 0.16); - background: #101716; - color: #f7f7f0; -} - -.hero::before { - position: absolute; - inset: 0; - content: ""; - background: - linear-gradient(rgba(247, 247, 240, 0.05) 1px, transparent 1px), - linear-gradient(90deg, rgba(247, 247, 240, 0.05) 1px, transparent 1px); - background-size: 52px 52px; - mask-image: linear-gradient(90deg, #000 0%, transparent 86%); -} - -.hero-inner { - position: relative; - z-index: 1; - max-width: 1220px; - margin: 0 auto; -} - -.hero-grid { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(320px, 420px); - gap: 54px; + grid-template-columns: minmax(0, 1fr) minmax(300px, 430px); + gap: 42px; align-items: center; -} - -.hero-copy { - min-width: 0; + max-width: 1240px; + min-height: min(760px, calc(100svh - 72px)); + margin: 0 auto; + padding: 86px 30px 54px; } .eyebrow { margin: 0 0 14px; - color: #8dd2c7; + color: var(--teal); font-size: 13px; - font-weight: 820; - text-transform: uppercase; + font-weight: 900; } h1, @@ -186,22 +110,26 @@ p { } h1 { - max-width: 780px; + display: grid; + gap: 0.06em; + max-width: 820px; margin: 0; - font-size: 112px; - line-height: 0.9; + font-size: clamp(48px, 5.8vw, 82px); + line-height: 1.04; + text-wrap: balance; + word-break: keep-all; } h2 { max-width: 820px; margin: 0; - font-size: 52px; - line-height: 1.02; + font-size: clamp(34px, 5vw, 58px); + line-height: 1.04; } h3 { margin: 0 0 10px; - font-size: 18px; + font-size: 19px; } p { @@ -210,13 +138,12 @@ p { } .lede { - max-width: 720px; + max-width: 760px; margin: 24px 0 0; - color: #dce8e2; font-size: 20px; } -.actions { +.hero-actions { flex-wrap: wrap; gap: 12px; margin-top: 34px; @@ -228,173 +155,185 @@ p { justify-content: center; min-height: 46px; padding: 12px 18px; - border: 1px solid rgba(247, 247, 240, 0.42); + border: 1px solid var(--ink); border-radius: 7px; - font-weight: 800; + font-weight: 900; } .button.primary { - border-color: #f7f7f0; - background: #f7f7f0; - color: var(--ink); + background: var(--ink); + color: var(--paper); } .button.secondary { - background: rgba(15, 107, 95, 0.8); - color: #f7f7f0; + background: var(--panel); +} + +.hero-visual { + min-width: 0; + padding: 22px; + border: 1px solid rgba(17, 22, 21, 0.22); + border-radius: 8px; + background: #101615; + color: #f9f1df; + box-shadow: 0 34px 90px rgba(17, 22, 21, 0.22); +} + +.repo-heading span, +.chart-heading, +dt { + color: #91cfc3; + font-size: 12px; + font-weight: 900; } -.button.ghost { - color: #f7f7f0; +.repo-heading strong { + display: block; + margin-top: 8px; + font-size: 34px; } -.proof-list { +.repo-files { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 0; - max-width: 980px; - margin: 30px 0 0; + gap: 10px; + margin: 24px 0; padding: 0; - border-top: 1px solid rgba(247, 247, 240, 0.24); - border-bottom: 1px solid rgba(247, 247, 240, 0.24); list-style: none; } -.proof-list li { - min-width: 0; - padding: 18px 18px 18px 0; +.repo-files li { + padding: 12px 13px; + overflow-wrap: anywhere; + border: 1px solid rgba(245, 241, 232, 0.16); + border-radius: 6px; + background: rgba(245, 241, 232, 0.05); + font-family: "SFMono-Regular", "JetBrains Mono", Consolas, monospace; + font-size: 13px; } -.proof-list li + li { - padding-left: 18px; - border-left: 1px solid rgba(247, 247, 240, 0.18); +.hero-metrics { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin: 0; + border-top: 1px solid rgba(245, 241, 232, 0.22); } -.proof-list strong, -.proof-list span { - display: block; +.hero-metrics div { + padding: 16px 10px 0; + border-right: 1px solid rgba(245, 241, 232, 0.18); } -.proof-list strong { - color: #ffffff; - font-size: 14px; - font-weight: 860; +.hero-metrics div:last-child { + border-right: 0; } -.proof-list span { - margin-top: 8px; - color: #c9d8d2; - font-size: 14px; - line-height: 1.55; +dd { + margin: 8px 0 0; + color: #fffaf0; + font-size: 25px; + font-weight: 900; } -.metric-strip { - align-items: stretch; - max-width: 860px; - margin: 32px 0 0; - padding: 0; - border: 1px solid rgba(247, 247, 240, 0.28); - border-radius: 8px; - background: rgba(247, 247, 240, 0.08); +.proof-strip { + max-width: 1240px; + margin: 0 auto 34px; + padding: 0 30px 56px; } -.metric-strip div { +.proof-strip article { flex: 1; min-width: 0; - padding: 18px; - border-right: 1px solid rgba(247, 247, 240, 0.22); + padding: 24px; + border: 1px solid var(--line); + background: var(--panel); } -.metric-strip div:last-child { - border-right: 0; +.proof-strip span, +.flow-grid span { + color: var(--red); + font-size: 13px; + font-weight: 900; } -.repo-visual { - min-width: 0; +.proof-strip h2 { + margin-top: 18px; + font-size: 22px; } -.repo-window { - padding: 24px; - border: 1px solid rgba(247, 247, 240, 0.22); - border-radius: 8px; - background: rgba(247, 247, 240, 0.08); - box-shadow: 0 28px 80px rgba(0, 0, 0, 0.26); +.flow, +.limits, +.benchmarks { + padding: 74px 30px; } -.repo-window span { - display: block; - color: #8dd2c7; - font-size: 12px; - font-weight: 820; - text-transform: uppercase; +.flow { + border-top: 1px solid var(--line); + background: #fffdf6; } -.repo-window strong { - display: block; - margin-top: 8px; - color: #ffffff; - font-size: 32px; +.section-heading, +.flow-grid, +.limits-grid, +.table-wrap, +.benchmark-lower { + width: 100%; + min-width: 0; + max-width: 1240px; + margin-right: auto; + margin-left: auto; } -.repo-window ul { - display: grid; - gap: 10px; - margin: 24px 0; - padding: 0; - list-style: none; +.section-heading { + margin-bottom: 30px; } -.repo-window li { - padding: 12px 14px; - overflow-wrap: anywhere; - border: 1px solid rgba(247, 247, 240, 0.16); - border-radius: 6px; - color: #dce8e2; - font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace; - font-size: 13px; +.section-heading p:last-child { + max-width: 780px; } -.repo-window p { - margin: 0; - color: #f7f7f0; - font-size: 14px; - font-weight: 780; +.flow-grid { + align-items: stretch; + border: 1px solid var(--line); + background: var(--panel); } -dt { - color: #b9c9c2; - font-size: 12px; - font-weight: 780; - text-transform: uppercase; +.flow-grid article { + flex: 1; + min-width: 0; + padding: 24px; + border-right: 1px solid var(--line); } -dd { - margin: 8px 0 0; - color: #ffffff; - font-size: 26px; - font-weight: 860; +.flow-grid article:last-child { + border-right: 0; } -.guarantees, -.benchmark-band, -.serverless { - padding: 70px 28px; +.limits { + background: #e7dfd0; } -.section-heading { - max-width: 1220px; - margin: 0 auto 30px; +.limits-grid { + gap: 16px; } -.section-heading p:last-child { - max-width: 760px; - margin-bottom: 0; +.limits-grid article { + flex: 1; + padding: 24px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); +} + +.limits-grid li { + margin: 10px 0; + color: var(--muted); +} + +.benchmarks { + background: var(--wash); } -.matrix-wrap, .table-wrap { - max-width: 1220px; - margin: 0 auto; overflow-x: auto; border: 1px solid var(--line); border-radius: 8px; @@ -421,10 +360,9 @@ th { text-transform: uppercase; } -td:first-child, th:first-child, -.matrix-table td, -.matrix-table th { +td:first-child, +td:nth-child(2) { text-align: left; } @@ -432,228 +370,107 @@ tbody tr:last-child td { border-bottom: 0; } -.benchmark-band { - border-top: 1px solid var(--line); - border-bottom: 1px solid var(--line); - background: #e7eef0; -} - -.comparison-grid { - align-items: stretch; - gap: 14px; - max-width: 1220px; - margin: 0 auto 22px; -} - -.operation-table-wrap { - margin-bottom: 22px; -} - -.operation-table { - min-width: 860px; -} - -.operation-table td:nth-child(1), -.operation-table td:nth-child(2) { - font-weight: 780; -} - -.operation-table td:nth-child(1) { - color: var(--green); -} - -.comparison { - flex: 1; - min-width: 0; - padding: 18px; - border: 1px solid rgba(17, 21, 20, 0.1); - border-left: 4px solid var(--green); - background: var(--panel); - box-shadow: 0 14px 32px rgba(17, 21, 20, 0.06); -} - -.comparison span { - display: block; - color: var(--muted); - font-size: 13px; -} - -.comparison strong { - display: block; - margin: 8px 0; - color: var(--ink); - font-size: 30px; +td:first-child { + color: var(--teal); + font-weight: 900; } -.benchmark-layout { +.benchmark-lower { align-items: stretch; - gap: 22px; - max-width: 1220px; - margin: 0 auto; + gap: 18px; + margin-top: 18px; } .chart-panel { flex: 0 0 370px; - min-height: 360px; padding: 22px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); } -.chart-axis { +.chart-heading { display: flex; justify-content: space-between; color: var(--muted); - font-size: 12px; - font-weight: 780; - text-transform: uppercase; } -.chart-bars { +.scenario-bars { display: grid; - gap: 17px; - margin-top: 24px; -} - -.bar-row { - display: grid; - gap: 8px; + gap: 15px; + margin-top: 22px; } .bar-label { display: flex; justify-content: space-between; gap: 12px; + margin-bottom: 7px; font-size: 13px; - font-weight: 760; + font-weight: 800; } .bar-track { - height: 16px; + height: 15px; overflow: hidden; border-radius: 4px; - background: #d8e0dd; + background: #cbd8d9; } .bar { min-width: 4px; - height: 16px; -} - -.bar.current { + height: 100%; background: var(--blue); } -.bar.accent { - background: var(--green); +.bar.fast { + background: var(--teal); } .bar.slow { - background: var(--rust); -} - -.serverless { - border-bottom: 1px solid var(--line); - background: #ffffff; -} - -.serverless-grid { - align-items: stretch; - gap: 14px; - max-width: 1220px; - margin: 0 auto; -} - -.serverless-grid article { - flex: 1; - min-width: 0; - padding: 24px; - border: 1px solid var(--line); - border-radius: 8px; - background: #f6f7f2; -} - -.serverless-grid span { - display: block; - margin-bottom: 24px; - color: var(--rust); - font-weight: 860; + background: var(--red); } footer { + align-items: center; justify-content: center; flex-wrap: wrap; gap: 22px; - min-height: 88px; - padding: 22px; + padding: 28px; border-top: 1px solid var(--line); color: var(--muted); - font-size: 14px; } -footer .footer-brand { +.footer-brand { color: var(--ink); - font-weight: 840; } -@media (max-width: 940px) { - .topbar { - align-items: flex-start; - flex-direction: column; - gap: 14px; - } - - .nav { - flex-wrap: wrap; - gap: 14px; - margin-left: 0; - } - +@media (max-width: 1040px) { .hero { - min-height: auto; - padding-top: 52px; - } - - .hero-grid { grid-template-columns: 1fr; + gap: 34px; + min-height: auto; } +} - h1 { - font-size: 78px; - } - - h2 { - font-size: 42px; - } - - .metric-strip, - .benchmark-layout, - .comparison-grid, - .serverless-grid { +@media (max-width: 920px) { + .topbar, + .proof-strip, + .flow-grid, + .limits-grid, + .benchmark-lower { flex-direction: column; } - .proof-list { - grid-template-columns: 1fr; - } - - .proof-list li { - padding: 16px 0; - } - - .proof-list li + li { - padding-left: 0; - border-top: 1px solid rgba(247, 247, 240, 0.18); - border-left: 0; - } - - .metric-strip div { - border-right: 0; - border-bottom: 1px solid rgba(247, 247, 240, 0.22); + nav { + flex-wrap: wrap; + justify-content: flex-end; + gap: 14px; } - .metric-strip div:last-child { + .proof-strip article, + .flow-grid article { + border-right: 1px solid var(--line); border-bottom: 0; } @@ -662,102 +479,40 @@ footer .footer-brand { } } -@media (max-width: 580px) { - .topbar, +@media (max-width: 620px) { + .topbar { + align-items: flex-start; + padding: 14px 18px; + } + .hero, - .guarantees, - .benchmark-band, - .serverless { + .flow, + .limits, + .benchmarks { padding-right: 18px; padding-left: 18px; } - .button, - .actions, - .language-switch { - width: 100%; + .proof-strip { + padding-right: 18px; + padding-left: 18px; } + .hero-actions, .button { - text-align: center; - } - - .language-switch button { - flex: 1; - } - - .lede { - font-size: 18px; - } - - h1 { - font-size: 58px; - } - - h2 { - font-size: 34px; - } - - dd { - font-size: 22px; - } - - .comparison strong { - font-size: 26px; - } - - .matrix-table { - min-width: 0; - } - - .matrix-table thead { - display: none; - } - - .matrix-table, - .matrix-table tbody, - .matrix-table tr, - .matrix-table td { - display: block; width: 100%; } - .matrix-table tr { - padding: 14px 0; - border-bottom: 1px solid var(--line); + .hero-metrics { + grid-template-columns: 1fr; } - .matrix-table tr:last-child { - border-bottom: 0; + .hero-metrics div { + border-right: 0; + border-bottom: 1px solid rgba(245, 241, 232, 0.18); } - .matrix-table td { - padding: 6px 16px; + .hero-metrics div:last-child { border-bottom: 0; } - - .matrix-table td:first-child { - color: var(--green); - font-weight: 840; - } - - .matrix-table td:nth-child(2)::before { - display: block; - margin-bottom: 4px; - color: var(--muted); - content: var(--matrix-guarantee-label); - font-size: 11px; - font-weight: 820; - text-transform: uppercase; - } - - .matrix-table td:nth-child(3)::before { - display: block; - margin-bottom: 4px; - color: var(--muted); - content: var(--matrix-boundary-label); - font-size: 11px; - font-weight: 820; - text-transform: uppercase; - } } diff --git a/tests/no-postgres-facade.test.ts b/tests/no-postgres-facade.test.ts index 89e8629..8122521 100644 --- a/tests/no-postgres-facade.test.ts +++ b/tests/no-postgres-facade.test.ts @@ -177,11 +177,13 @@ describe("PostgreSQL facade removal", () => { // When/Then: both examples use fetch/Web Crypto stores instead of Node-only Octokit stores. expect(plaintext).toContain("../../dist/src/browser.js") expect(plaintext).toContain("GitHubFetchPlaintextStore") - expect(plaintext).not.toMatch(/Octokit|GitHubPlaintextStore/) + expect(plaintext).not.toMatch(/Octokit|GitHubPlaintextStore|\bfetch\(|githubContentsUrl/) expect(encrypted).toContain("../../dist/src/browser.js") expect(encrypted).toContain("GitHubFetchEncryptedStore") expect(encrypted).toContain("createWebAesGcmCipher") - expect(encrypted).not.toMatch(/Octokit|GitHubEncryptedStore|createAesGcmCipher/) + expect(encrypted).not.toMatch( + /Octokit|GitHubEncryptedStore|createAesGcmCipher|\bfetch\(|githubContentsUrl/, + ) }) it("removes stale generated-example paths from tool config", async () => { diff --git a/tests/site.test.ts b/tests/site.test.ts index fa143d9..8b55949 100644 --- a/tests/site.test.ts +++ b/tests/site.test.ts @@ -2,7 +2,6 @@ import { readFile } from "node:fs/promises" import { describe, expect, it } from "vitest" type BenchmarkEvidence = { - readonly comparisons?: readonly unknown[] readonly current?: { readonly metadata?: unknown } @@ -18,89 +17,55 @@ type BenchmarkScenario = { readonly key: string } -describe("website", () => { - it("publishes benchmark evidence consumed by the static site", async () => { - const parsed: unknown = JSON.parse(await readFile("site/benchmark.json", "utf8")) +describe("static website", () => { + it("ships a Korean-first product site with benchmark evidence", async () => { + // Given: GitHub Pages deploys the static site directory as the product website. + const [workflow, index, app, styles] = await Promise.all([ + readFile(".github/workflows/pages.yml", "utf8"), + readFile("site/index.html", "utf8"), + readFile("site/app.js", "utf8"), + readFile("site/styles.css", "utf8"), + ]) + const benchmark: unknown = JSON.parse(await readFile("site/benchmark.json", "utf8")) - expect(isBenchmarkEvidence(parsed)).toBe(true) - if (!isBenchmarkEvidence(parsed)) { + // When: the deploy artifact and benchmark payload are inspected. + expect(isBenchmarkEvidence(benchmark)).toBe(true) + if (!isBenchmarkEvidence(benchmark)) { throw new Error("invalid benchmark evidence") } - expect(parsed.scenarios.map((scenario) => scenario.key)).toEqual( + + // Then: the website has the service positioning, benchmarks, and runtime wiring. + expect(workflow).toContain("path: site") + expect(index).toContain('') + expect(index).toContain("GitHub 기반 서버리스 RDB") + expect(index).toContain("벤치마크") + expect(index).toContain('id="operation-rows"') + expect(index).toContain('id="scenario-bars"') + expect(index).toContain("docs/BENCHMARKS.md") + expect(app).toContain("./benchmark.json") + expect(app).toContain("github encrypted-delete-filter") + expect(app).toContain("table-insert-many") + expect(styles).toContain(".hero-visual") + expect(styles).toContain("@media") + expect(benchmark.current?.metadata).toBeDefined() + expect(benchmark.scenarios.map((scenario) => scenario.key)).toEqual( expect.arrayContaining([ "local-plaintext", "local-encrypted", "github-plaintext", "github-encrypted", - "table-upsert-single", "table-insert-many", - "table-upsert-many", "table-indexed-lookup", - "reopen", - "compaction", ]), ) - expect(parsed.scenarios.length).toBeGreaterThanOrEqual(8) - expect(parsed.operations.map((operation) => operation.key)).toEqual( + expect(benchmark.operations.map((operation) => operation.key)).toEqual( expect.arrayContaining([ "plaintext-insert-batch", "plaintext-select-filter", - "plaintext-join", - "plaintext-delete-filter", - "encrypted-insert-batch", - "encrypted-select-filter", - "encrypted-join", - "encrypted-delete-filter", "github plaintext-insert-batch", - "github plaintext-select-filter", - "github plaintext-join", - "github plaintext-delete-filter", - "github encrypted-insert-batch", - "github encrypted-select-filter", - "github encrypted-join", "github encrypted-delete-filter", ]), ) - expect(parsed.current?.metadata).toBeDefined() - }) - - it("deploys the site directory through GitHub Pages", async () => { - const workflow = await readFile(".github/workflows/pages.yml", "utf8") - const index = await readFile("site/index.html", "utf8") - const app = await readFile("site/app.js", "utf8") - const content = await readFile("site/content.js", "utf8") - - expect(workflow).toContain("path: site") - expect(app).toContain("./benchmark.json") - expect(app).toContain("./content.js") - expect(app).toContain("operation-rows") - expect(app).toContain("table-indexed-lookup") - expect(index).toContain('data-language-option="en"') - expect(index).toContain('data-language-option="ko"') - expect(index).toContain("Serverless RDB on GitHub") - expect(index).toContain("Use GitHub as the database, not just file storage.") - expect(index).toContain("Guarantee matrix") - expect(index).toContain("Transactions") - expect(index).toContain("Compaction") - expect(index).toContain('id="serverless"') - expect(index).not.toContain("./assets/runtime-map.svg") - expect(index).not.toContain("@3xhaust/gitdb/browser") - expect(index).not.toContain("docs/API.md") - expect(index).toContain("docs/RELEASE.md") - expect(content).toContain("Serverless RDB on GitHub") - expect(content).toContain("GitHub 기반 서버리스 RDB") - expect(content).toContain("GitDB behaves like a small RDB") - expect(content).toContain("GitDB는 작은 RDB처럼 동작") - expect(content).toContain("What common database calls cost") - expect(content).toContain("자주 쓰는 DB 작업") - expect(content).toContain("plaintext-select-filter") - expect(content).toContain("encrypted-delete-filter") - expect(content).toContain("github plaintext-insert-batch") - expect(content).toContain("github encrypted-delete-filter") - expect(content).not.toContain("client runtime") - expect(content).not.toContain("browser entrypoint") - expect(index).not.toMatch(/PostgreSQL|Prisma|facade|postgresql:\/\//i) - expect(index).not.toMatch(/audit sync|Audit storage/i) }) })