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

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-B4o5hA96.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-B4o5hA96.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,
@@ -435,14 +437,12 @@ function parseUpstream(rawPathname, rawSearch = "") {
435
437
  const hadDotGit = Boolean(match[3]);
436
438
  const operation = match[4];
437
439
  let host = authority;
438
- let port = 443;
439
440
  const colon = authority.lastIndexOf(":");
440
441
  if (colon !== -1) {
441
442
  host = authority.slice(0, colon);
442
443
  const p = authority.slice(colon + 1);
443
444
  if (!/^\d{1,5}$/.test(p)) return null;
444
- port = Number(p);
445
- if (port !== 443) return null;
445
+ if (Number(p) !== 443) return null;
446
446
  }
447
447
  host = host.toLowerCase();
448
448
  if (isIpLiteral(host)) return null;
@@ -1311,7 +1311,27 @@ async function refreshMirror(repo, local, credHeader = null) {
1311
1311
  log("info", "refresh", { repo });
1312
1312
  return local;
1313
1313
  }
1314
- async function ensureWarm(repo, credHeader = null) {
1314
+ /**
1315
+ * Decide how to serve a warm mirror for one request.
1316
+ *
1317
+ * `revalidateRefs` is set for the `info/refs` half of a fetch — the ref
1318
+ * advertisement, where the client learns the remote's HEAD and branch tips.
1319
+ * That answer must never be stale: serving a pre-merge advertisement is how a
1320
+ * `git fetch` right after an upstream squash-merge silently returns the old
1321
+ * `origin/<branch>` (the object-reuse win of the cache is not worth lying about
1322
+ * where HEAD is). So for `info/refs` a stale mirror BLOCKS on a synchronous
1323
+ * refresh before it answers, exactly as an explicit push invalidation does.
1324
+ * The `git-upload-pack` half keeps stale-while-revalidate: its wants come from
1325
+ * the just-served fresh advertisement, so the client only asks for objects the
1326
+ * fresh refs named, and the caller's `mirrorHasObjects` gate already falls back
1327
+ * to origin for any the mirror is missing (the new objects a merge introduced).
1328
+ *
1329
+ * A blocked refresh that FAILS (network blip, expired token, upstream 5xx)
1330
+ * returns `cacheState: 'STALE_REVALIDATE_FAILED'` rather than throwing, so the
1331
+ * caller can proxy `info/refs` straight to origin — fresh refs from upstream,
1332
+ * never a 502 and never the stale advertisement.
1333
+ */
1334
+ async function ensureWarm(repo, credHeader = null, revalidateRefs = false) {
1315
1335
  const local = localPath(repo);
1316
1336
  const lastFetchMs = (await readMeta(local))?.lastFetchMs ?? 0;
1317
1337
  if (!((Date.now() - lastFetchMs) / 1e3 >= config.ttlSeconds || lastFetchMs < invalidateBeforeMs)) {
@@ -1321,7 +1341,8 @@ async function ensureWarm(repo, credHeader = null) {
1321
1341
  cacheState: "HIT"
1322
1342
  };
1323
1343
  }
1324
- const mustBlock = lastFetchMs < invalidateBeforeMs;
1344
+ const fencedByPush = lastFetchMs < invalidateBeforeMs;
1345
+ const mustBlock = fencedByPush || revalidateRefs;
1325
1346
  if (!inflightRefresh.has(repo)) inflightRefresh.set(repo, withRepoLock(repo, "write", () => refreshMirror(repo, local, credHeader)).then((result) => {
1326
1347
  if (result === null) {
1327
1348
  metrics.refresh_deferred++;
@@ -1336,7 +1357,19 @@ async function ensureWarm(repo, credHeader = null) {
1336
1357
  }).finally(() => inflightRefresh.delete(repo)));
1337
1358
  if (mustBlock) {
1338
1359
  await inflightRefresh.get(repo);
1339
- if (((await readMeta(local))?.lastFetchMs ?? 0) < invalidateBeforeMs) throw new Error("mirror refresh failed after push invalidation");
1360
+ const refreshedMs = (await readMeta(local))?.lastFetchMs ?? 0;
1361
+ if (!(refreshedMs >= invalidateBeforeMs && (Date.now() - refreshedMs) / 1e3 < config.ttlSeconds)) {
1362
+ if (revalidateRefs && !fencedByPush) {
1363
+ metrics.revalidate_failures++;
1364
+ log("warn", "revalidate_failed", { repo });
1365
+ return {
1366
+ local,
1367
+ cacheState: "STALE_REVALIDATE_FAILED"
1368
+ };
1369
+ }
1370
+ throw new Error("mirror refresh failed after invalidation");
1371
+ }
1372
+ if (revalidateRefs) metrics.revalidations++;
1340
1373
  return {
1341
1374
  local,
1342
1375
  cacheState: "REFRESH"
@@ -1610,6 +1643,42 @@ function isLoopbackPeer(req) {
1610
1643
  const addr = req.socket.remoteAddress;
1611
1644
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
1612
1645
  }
1646
+ /**
1647
+ * Proxy an `info/refs` advertisement straight to origin and stream it back to
1648
+ * the client, retaining a bounded copy of a v0 advertisement for later ref
1649
+ * completion. Used for a cold miss AND when a warm mirror's blocking
1650
+ * revalidation failed — either way the client gets a FRESH advertisement from
1651
+ * upstream, never the mirror's stale refs.
1652
+ */
1653
+ async function proxyInfoRefs(req, res, repo, upstreamUrl, host, credHeader) {
1654
+ const cred = credHeader ?? await fetchCredentialHeader(host);
1655
+ await new Promise((resolve, reject) => {
1656
+ openUpstream(req, upstreamUrl, "info/refs", (upRes, error) => {
1657
+ if (error) {
1658
+ reject(error);
1659
+ return;
1660
+ }
1661
+ if (upRes.statusCode === 401 || upRes.statusCode === 403) dropCredential(host);
1662
+ res.writeHead(upRes.statusCode, upRes.headers);
1663
+ const chunks = [];
1664
+ let bytes = 0;
1665
+ const CAP = 32 * 1024 * 1024;
1666
+ upRes.on("data", (chunk) => {
1667
+ res.write(chunk);
1668
+ if (bytes < CAP) {
1669
+ chunks.push(chunk);
1670
+ bytes += chunk.length;
1671
+ }
1672
+ });
1673
+ upRes.on("end", () => {
1674
+ res.end();
1675
+ if (upRes.statusCode === 200 && bytes < CAP && gitProtocolEnv(req.headers).GIT_PROTOCOL === void 0) advertisements.set(repo, parseAdvertisement(Buffer.concat(chunks)));
1676
+ resolve();
1677
+ });
1678
+ upRes.on("error", reject);
1679
+ }, cred);
1680
+ });
1681
+ }
1613
1682
  async function handle(req, res) {
1614
1683
  const parsed = new URL(req.url, "http://git-cache.local");
1615
1684
  if (parsed.pathname === "/healthz") {
@@ -1685,7 +1754,7 @@ async function handle(req, res) {
1685
1754
  }
1686
1755
  clientCred = raw;
1687
1756
  }
1688
- const host = up.repo.split("/")[0];
1757
+ const host = up.repo.split("/")[0] ?? "";
1689
1758
  let uploadBody = null;
1690
1759
  let uploadWants = [];
1691
1760
  if (up.operation === "git-upload-pack") {
@@ -1706,8 +1775,16 @@ async function handle(req, res) {
1706
1775
  uploadWants = classifyUploadRequest(uploadBody).wants;
1707
1776
  }
1708
1777
  if (await pathExists(localPath(up.repo))) {
1709
- const warm = await ensureWarm(up.repo, clientCred);
1778
+ const warm = await ensureWarm(up.repo, clientCred, up.operation === "info/refs");
1710
1779
  markServed(warm.local);
1780
+ if (warm.cacheState === "STALE_REVALIDATE_FAILED") {
1781
+ log("info", "serve", {
1782
+ repo: up.repo,
1783
+ op: up.operation,
1784
+ state: warm.cacheState
1785
+ });
1786
+ return proxyInfoRefs(req, res, up.repo, up.upstreamUrl, host, clientCred);
1787
+ }
1711
1788
  if (up.operation === "git-upload-pack" && !await mirrorHasObjects(warm.local, uploadWants)) {
1712
1789
  await refreshMirror(up.repo, warm.local, clientCred).catch((error) => {
1713
1790
  metrics.refresh_failures++;
@@ -1730,35 +1807,7 @@ async function handle(req, res) {
1730
1807
  });
1731
1808
  return serveUploadPack(req, res, warm.local, up.operation === "info/refs", warm.cacheState, uploadBody);
1732
1809
  }
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
- }
1810
+ if (up.operation === "info/refs") return proxyInfoRefs(req, res, up.repo, up.upstreamUrl, host, clientCred);
1762
1811
  const body = uploadBody ?? Buffer.alloc(0);
1763
1812
  const credHeader = clientCred ?? await fetchCredentialHeader(host);
1764
1813
  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.1642",
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",