@skydiveai/git-cache 0.1.0-beta.1480 → 0.1.0-beta.1635

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/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { f as startServer } from "./server-B5u5DETT.mjs";
2
+ import { f as startServer } from "./server-DfaJ_V9K.mjs";
3
3
  //#region src/cli.ts
4
4
  /**
5
5
  * `git-cache` CLI — the published binary for @skydiveai/git-cache.
package/dist/index.d.mts CHANGED
@@ -19,6 +19,8 @@ declare function metricsSnapshot(): {
19
19
  evictions: number;
20
20
  refresh_failures: number;
21
21
  refresh_deferred: number;
22
+ revalidations: number;
23
+ revalidate_failures: number;
22
24
  publish_deferred: number;
23
25
  credential_url_redirects: number;
24
26
  lfs_redirects: number;
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as isProtocolV2Request, c as parseAdvertisement, d as shouldBackfillAfterPassthrough, f as startServer, i as explainRejection, l as parseUpstream, n as classifyUploadRequest, o as metricsSnapshot, r as copyRequestHeaders, s as mirrorHasObjects, t as SidebandPackExtractor, u as recordColdBuildOutcome } from "./server-B5u5DETT.mjs";
1
+ import { a as isProtocolV2Request, c as parseAdvertisement, d as shouldBackfillAfterPassthrough, f as startServer, i as explainRejection, l as parseUpstream, n as classifyUploadRequest, o as metricsSnapshot, r as copyRequestHeaders, s as mirrorHasObjects, t as SidebandPackExtractor, u as recordColdBuildOutcome } from "./server-DfaJ_V9K.mjs";
2
2
  export { SidebandPackExtractor, classifyUploadRequest, copyRequestHeaders, explainRejection, isProtocolV2Request, metricsSnapshot, mirrorHasObjects, parseAdvertisement, parseUpstream, recordColdBuildOutcome, shouldBackfillAfterPassthrough, startServer };
@@ -116,6 +116,8 @@ const metrics = {
116
116
  evictions: 0,
117
117
  refresh_failures: 0,
118
118
  refresh_deferred: 0,
119
+ revalidations: 0,
120
+ revalidate_failures: 0,
119
121
  publish_deferred: 0,
120
122
  credential_url_redirects: 0,
121
123
  lfs_redirects: 0,
@@ -1311,7 +1313,27 @@ async function refreshMirror(repo, local, credHeader = null) {
1311
1313
  log("info", "refresh", { repo });
1312
1314
  return local;
1313
1315
  }
1314
- async function ensureWarm(repo, credHeader = null) {
1316
+ /**
1317
+ * Decide how to serve a warm mirror for one request.
1318
+ *
1319
+ * `revalidateRefs` is set for the `info/refs` half of a fetch — the ref
1320
+ * advertisement, where the client learns the remote's HEAD and branch tips.
1321
+ * That answer must never be stale: serving a pre-merge advertisement is how a
1322
+ * `git fetch` right after an upstream squash-merge silently returns the old
1323
+ * `origin/<branch>` (the object-reuse win of the cache is not worth lying about
1324
+ * where HEAD is). So for `info/refs` a stale mirror BLOCKS on a synchronous
1325
+ * refresh before it answers, exactly as an explicit push invalidation does.
1326
+ * The `git-upload-pack` half keeps stale-while-revalidate: its wants come from
1327
+ * the just-served fresh advertisement, so the client only asks for objects the
1328
+ * fresh refs named, and the caller's `mirrorHasObjects` gate already falls back
1329
+ * to origin for any the mirror is missing (the new objects a merge introduced).
1330
+ *
1331
+ * A blocked refresh that FAILS (network blip, expired token, upstream 5xx)
1332
+ * returns `cacheState: 'STALE_REVALIDATE_FAILED'` rather than throwing, so the
1333
+ * caller can proxy `info/refs` straight to origin — fresh refs from upstream,
1334
+ * never a 502 and never the stale advertisement.
1335
+ */
1336
+ async function ensureWarm(repo, credHeader = null, revalidateRefs = false) {
1315
1337
  const local = localPath(repo);
1316
1338
  const lastFetchMs = (await readMeta(local))?.lastFetchMs ?? 0;
1317
1339
  if (!((Date.now() - lastFetchMs) / 1e3 >= config.ttlSeconds || lastFetchMs < invalidateBeforeMs)) {
@@ -1321,7 +1343,8 @@ async function ensureWarm(repo, credHeader = null) {
1321
1343
  cacheState: "HIT"
1322
1344
  };
1323
1345
  }
1324
- const mustBlock = lastFetchMs < invalidateBeforeMs;
1346
+ const fencedByPush = lastFetchMs < invalidateBeforeMs;
1347
+ const mustBlock = fencedByPush || revalidateRefs;
1325
1348
  if (!inflightRefresh.has(repo)) inflightRefresh.set(repo, withRepoLock(repo, "write", () => refreshMirror(repo, local, credHeader)).then((result) => {
1326
1349
  if (result === null) {
1327
1350
  metrics.refresh_deferred++;
@@ -1336,7 +1359,19 @@ async function ensureWarm(repo, credHeader = null) {
1336
1359
  }).finally(() => inflightRefresh.delete(repo)));
1337
1360
  if (mustBlock) {
1338
1361
  await inflightRefresh.get(repo);
1339
- if (((await readMeta(local))?.lastFetchMs ?? 0) < invalidateBeforeMs) throw new Error("mirror refresh failed after push invalidation");
1362
+ const refreshedMs = (await readMeta(local))?.lastFetchMs ?? 0;
1363
+ if (!(refreshedMs >= invalidateBeforeMs && (Date.now() - refreshedMs) / 1e3 < config.ttlSeconds)) {
1364
+ if (revalidateRefs && !fencedByPush) {
1365
+ metrics.revalidate_failures++;
1366
+ log("warn", "revalidate_failed", { repo });
1367
+ return {
1368
+ local,
1369
+ cacheState: "STALE_REVALIDATE_FAILED"
1370
+ };
1371
+ }
1372
+ throw new Error("mirror refresh failed after invalidation");
1373
+ }
1374
+ if (revalidateRefs) metrics.revalidations++;
1340
1375
  return {
1341
1376
  local,
1342
1377
  cacheState: "REFRESH"
@@ -1610,6 +1645,42 @@ function isLoopbackPeer(req) {
1610
1645
  const addr = req.socket.remoteAddress;
1611
1646
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
1612
1647
  }
1648
+ /**
1649
+ * Proxy an `info/refs` advertisement straight to origin and stream it back to
1650
+ * the client, retaining a bounded copy of a v0 advertisement for later ref
1651
+ * completion. Used for a cold miss AND when a warm mirror's blocking
1652
+ * revalidation failed — either way the client gets a FRESH advertisement from
1653
+ * upstream, never the mirror's stale refs.
1654
+ */
1655
+ async function proxyInfoRefs(req, res, repo, upstreamUrl, host, credHeader) {
1656
+ const cred = credHeader ?? await fetchCredentialHeader(host);
1657
+ await new Promise((resolve, reject) => {
1658
+ openUpstream(req, upstreamUrl, "info/refs", (upRes, error) => {
1659
+ if (error) {
1660
+ reject(error);
1661
+ return;
1662
+ }
1663
+ if (upRes.statusCode === 401 || upRes.statusCode === 403) dropCredential(host);
1664
+ res.writeHead(upRes.statusCode, upRes.headers);
1665
+ const chunks = [];
1666
+ let bytes = 0;
1667
+ const CAP = 32 * 1024 * 1024;
1668
+ upRes.on("data", (chunk) => {
1669
+ res.write(chunk);
1670
+ if (bytes < CAP) {
1671
+ chunks.push(chunk);
1672
+ bytes += chunk.length;
1673
+ }
1674
+ });
1675
+ upRes.on("end", () => {
1676
+ res.end();
1677
+ if (upRes.statusCode === 200 && bytes < CAP && gitProtocolEnv(req.headers).GIT_PROTOCOL === void 0) advertisements.set(repo, parseAdvertisement(Buffer.concat(chunks)));
1678
+ resolve();
1679
+ });
1680
+ upRes.on("error", reject);
1681
+ }, cred);
1682
+ });
1683
+ }
1613
1684
  async function handle(req, res) {
1614
1685
  const parsed = new URL(req.url, "http://git-cache.local");
1615
1686
  if (parsed.pathname === "/healthz") {
@@ -1685,7 +1756,7 @@ async function handle(req, res) {
1685
1756
  }
1686
1757
  clientCred = raw;
1687
1758
  }
1688
- const host = up.repo.split("/")[0];
1759
+ const host = up.repo.split("/")[0] ?? "";
1689
1760
  let uploadBody = null;
1690
1761
  let uploadWants = [];
1691
1762
  if (up.operation === "git-upload-pack") {
@@ -1706,8 +1777,16 @@ async function handle(req, res) {
1706
1777
  uploadWants = classifyUploadRequest(uploadBody).wants;
1707
1778
  }
1708
1779
  if (await pathExists(localPath(up.repo))) {
1709
- const warm = await ensureWarm(up.repo, clientCred);
1780
+ const warm = await ensureWarm(up.repo, clientCred, up.operation === "info/refs");
1710
1781
  markServed(warm.local);
1782
+ if (warm.cacheState === "STALE_REVALIDATE_FAILED") {
1783
+ log("info", "serve", {
1784
+ repo: up.repo,
1785
+ op: up.operation,
1786
+ state: warm.cacheState
1787
+ });
1788
+ return proxyInfoRefs(req, res, up.repo, up.upstreamUrl, host, clientCred);
1789
+ }
1711
1790
  if (up.operation === "git-upload-pack" && !await mirrorHasObjects(warm.local, uploadWants)) {
1712
1791
  await refreshMirror(up.repo, warm.local, clientCred).catch((error) => {
1713
1792
  metrics.refresh_failures++;
@@ -1730,35 +1809,7 @@ async function handle(req, res) {
1730
1809
  });
1731
1810
  return serveUploadPack(req, res, warm.local, up.operation === "info/refs", warm.cacheState, uploadBody);
1732
1811
  }
1733
- if (up.operation === "info/refs") {
1734
- const credHeader = clientCred ?? await fetchCredentialHeader(host);
1735
- return await new Promise((resolve, reject) => {
1736
- openUpstream(req, up.upstreamUrl, up.operation, (upRes, error) => {
1737
- if (error) {
1738
- reject(error);
1739
- return;
1740
- }
1741
- if (upRes.statusCode === 401 || upRes.statusCode === 403) dropCredential(host);
1742
- res.writeHead(upRes.statusCode, upRes.headers);
1743
- const chunks = [];
1744
- let bytes = 0;
1745
- const CAP = 32 * 1024 * 1024;
1746
- upRes.on("data", (chunk) => {
1747
- res.write(chunk);
1748
- if (bytes < CAP) {
1749
- chunks.push(chunk);
1750
- bytes += chunk.length;
1751
- }
1752
- });
1753
- upRes.on("end", () => {
1754
- res.end();
1755
- if (upRes.statusCode === 200 && bytes < CAP && gitProtocolEnv(req.headers).GIT_PROTOCOL === void 0) advertisements.set(up.repo, parseAdvertisement(Buffer.concat(chunks)));
1756
- resolve(void 0);
1757
- });
1758
- upRes.on("error", reject);
1759
- }, credHeader);
1760
- });
1761
- }
1812
+ if (up.operation === "info/refs") return proxyInfoRefs(req, res, up.repo, up.upstreamUrl, host, clientCred);
1762
1813
  const body = uploadBody ?? Buffer.alloc(0);
1763
1814
  const credHeader = clientCred ?? await fetchCredentialHeader(host);
1764
1815
  if (coldLeaders.has(up.repo)) return proxyColdFollowerWithBody(res, up.upstreamUrl, req.headers, body, up.repo, credHeader);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skydiveai/git-cache",
3
- "version": "0.1.0-beta.1480",
3
+ "version": "0.1.0-beta.1635",
4
4
  "description": "Sandbox-local Git smart-HTTP clone/fetch cache: a loopback daemon that transparently mirrors HTTPS git reads so repeated clone/fetch/ls-remote skip the origin round-trip. Runs inside an agent sandbox; pushes stay direct.",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",