@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 +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -1
- package/dist/{server-B5u5DETT.mjs → server-B4o5hA96.mjs} +86 -37
- 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-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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|