@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 +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -1
- package/dist/{server-B5u5DETT.mjs → server-DfaJ_V9K.mjs} +85 -34
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|