git-fs-s3 0.14.0 → 0.15.0
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/README.md +15 -1
- package/dist/chunk-HLYCSNJ2.js +53 -0
- package/dist/chunk-HLYCSNJ2.js.map +1 -0
- package/dist/{chunk-RJYNJOYF.js → chunk-IAKZRWBZ.js} +2 -2
- package/dist/{chunk-HO6OPXUS.js → chunk-OKBQISUC.js} +5 -1
- package/dist/chunk-OKBQISUC.js.map +1 -0
- package/dist/{chunk-G636FB7W.js → chunk-T5M5EYCZ.js} +2 -2
- package/dist/fetch-s3.js +174 -0
- package/dist/fetch-s3.js.map +1 -0
- package/dist/http.js +2 -2
- package/dist/index.js +42 -53
- package/dist/index.js.map +1 -1
- package/dist/node.js +74 -0
- package/dist/node.js.map +1 -0
- package/dist/ops.js +3 -3
- package/dist/types/git-errors.d.ts +2 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/prefix.d.ts +23 -0
- package/dist/types/retry.d.ts +7 -0
- package/dist/types/stores/fetch-s3.d.ts +55 -0
- package/dist/types/stores/file.d.ts +16 -0
- package/package.json +14 -1
- package/dist/chunk-HO6OPXUS.js.map +0 -1
- /package/dist/{chunk-RJYNJOYF.js.map → chunk-IAKZRWBZ.js.map} +0 -0
- /package/dist/{chunk-G636FB7W.js.map → chunk-T5M5EYCZ.js.map} +0 -0
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ Use it to build git-backed CMSes, notes apps with real version history, lightwei
|
|
|
34
34
|
npm install git-fs-s3 isomorphic-git # ESM only, Node.js 22+
|
|
35
35
|
# for the S3 store (any S3-compatible provider):
|
|
36
36
|
npm install @aws-sdk/client-s3
|
|
37
|
+
# or, for the fetch-based store (no AWS SDK):
|
|
38
|
+
npm install aws4fetch
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
## Quick start
|
|
@@ -175,6 +177,14 @@ range reads of the pack.
|
|
|
175
177
|
|
|
176
178
|
`ObjectStore` over `@aws-sdk/client-s3` (optional peer dependency, loaded only via the `/s3` subpath). Implements the optional `getRange(key, start, end)` (S3's `Range` header, inclusive at both ends), which `getObjectSize` uses. Reuse one `S3Client` across stores so HTTP connections are pooled — a busy repo page can mean hundreds of object reads. `contentType(key)` optionally derives a `Content-Type` header for uploads.
|
|
177
179
|
|
|
180
|
+
### `createFetchS3Store({ endpoint, bucket, accessKeyId, secretAccessKey, region?, contentType?, fetch? })`
|
|
181
|
+
|
|
182
|
+
From `git-fs-s3/fetch-s3`. An S3/R2/MinIO store over plain `fetch`, signed with [`aws4fetch`](https://github.com/mhart/aws4fetch) (optional peer dependency: `npm install aws4fetch`) — no AWS SDK, so it runs on Node, Vercel Functions and Workers alike. Implements `getRange`, ListObjectsV2 pagination and delimiters, and a server-side `copy(from, to)` (pass it to `movePrefix`). A 404 maps to `null`; any other failure throws `S3HttpError` with `$metadata.httpStatusCode` (429 is named `SlowDown`), which `isRetryableError` classifies. `region` defaults to `"auto"` (R2); timeouts are `bodyTimeoutMs` (30 s) and `metadataTimeoutMs` (10 s) per attempt. Retries are not built in — wrap it in `createRetryStore`.
|
|
183
|
+
|
|
184
|
+
### `FileObjectStore(root)`
|
|
185
|
+
|
|
186
|
+
From `git-fs-s3/node` (Node only, so edge bundles stay clean). A disk-backed store for local development: reads are served from memory, every write is mirrored to a file under `root`, and the tree is loaded back on first use, so repositories survive restarts. Keys that would escape `root` are rejected. Not for production.
|
|
187
|
+
|
|
178
188
|
### `MemoryObjectStore()`
|
|
179
189
|
|
|
180
190
|
In-memory store; also the reference implementation of list/delimiter semantics.
|
|
@@ -198,10 +208,14 @@ Wraps any store with an in-process LRU read cache (git objects are content-addre
|
|
|
198
208
|
Wraps any store with exponential-backoff retries and a per-instance circuit breaker. Place it directly above the network store, under the cache.
|
|
199
209
|
|
|
200
210
|
- `retries` (3), `initialDelayMs` (100), `maxDelayMs` (5000), `jitter` (0.3)
|
|
201
|
-
- `isRetryable(error)` — default
|
|
211
|
+
- `isRetryable(error)` — default is the exported `isRetryableError`: network faults (including `fetch`'s bare `TypeError`), timeouts, throttling, and HTTP 5xx/429. Compose it in your own classifier.
|
|
202
212
|
- `breaker` — `{ threshold: 5, resetMs: 30_000 }` by default, or `false` to disable. While open, calls fail fast with `CircuitOpenError` (`code: "EUNAVAILABLE"`).
|
|
203
213
|
- `onRetry(info)` — logging hook.
|
|
204
214
|
|
|
215
|
+
### Prefix helpers and error classification
|
|
216
|
+
|
|
217
|
+
`deletePrefix(store, prefix)` lists a prefix and deletes it in batches. `movePrefix(store, from, to, { copy? })` copies everything, then deletes the old keys only once every copy succeeded (a failed copy leaves the source untouched); pass a store's server-side `copy` to avoid pulling bytes through the process. Both return the object count, and neither touches caches above the store — call `invalidate(prefix)` yourself and hold your own lock. `isNotFoundError(error)` is true for the missing repository/ref/path/object errors, the ones to map to a 404.
|
|
218
|
+
|
|
205
219
|
## Git smart-HTTP
|
|
206
220
|
|
|
207
221
|
`git-fs-s3/http` implements the git smart-HTTP protocol (`info/refs`, `upload-pack`, `receive-pack`) as plain functions over a `Repo` — no framework assumptions, Fetch-API-shaped inputs/outputs. Extracted from the same production git-hosting service's HTTP layer, so it's what actually serves `git clone`/`git push` over HTTPS: pkt-line framing, side-band-64k packfile chunking (required once a client like isomorphic-git negotiates it — it always demuxes the response), CAS-checked ref updates, and pack consolidation.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/stores/memory.ts
|
|
2
|
+
var MemoryObjectStore = class {
|
|
3
|
+
objects = /* @__PURE__ */ new Map();
|
|
4
|
+
async get(key) {
|
|
5
|
+
const data = this.objects.get(key);
|
|
6
|
+
return data ? data.slice() : null;
|
|
7
|
+
}
|
|
8
|
+
async getRange(key, start, end) {
|
|
9
|
+
const data = this.objects.get(key);
|
|
10
|
+
return data ? data.slice(start, end + 1) : null;
|
|
11
|
+
}
|
|
12
|
+
async put(key, data) {
|
|
13
|
+
this.objects.set(key, data.slice());
|
|
14
|
+
}
|
|
15
|
+
async delete(key) {
|
|
16
|
+
this.objects.delete(key);
|
|
17
|
+
}
|
|
18
|
+
async head(key) {
|
|
19
|
+
const data = this.objects.get(key);
|
|
20
|
+
return data ? { size: data.byteLength } : null;
|
|
21
|
+
}
|
|
22
|
+
async list(prefix, options) {
|
|
23
|
+
const delimiter = options?.delimiter;
|
|
24
|
+
const limit = options?.limit ?? Number.POSITIVE_INFINITY;
|
|
25
|
+
const objects = [];
|
|
26
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
27
|
+
for (const [key, data] of this.objects) {
|
|
28
|
+
if (!key.startsWith(prefix)) continue;
|
|
29
|
+
const rest = key.slice(prefix.length);
|
|
30
|
+
if (delimiter !== void 0) {
|
|
31
|
+
const idx = rest.indexOf(delimiter);
|
|
32
|
+
if (idx !== -1) {
|
|
33
|
+
prefixes.add(prefix + rest.slice(0, idx + delimiter.length));
|
|
34
|
+
} else {
|
|
35
|
+
objects.push({ key, size: data.byteLength });
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
objects.push({ key, size: data.byteLength });
|
|
39
|
+
}
|
|
40
|
+
if (objects.length + prefixes.size >= limit) break;
|
|
41
|
+
}
|
|
42
|
+
return { objects, prefixes: [...prefixes] };
|
|
43
|
+
}
|
|
44
|
+
/** Number of stored objects (test convenience, not part of ObjectStore). */
|
|
45
|
+
get size() {
|
|
46
|
+
return this.objects.size;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
MemoryObjectStore
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=chunk-HLYCSNJ2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stores/memory.ts"],"sourcesContent":["import type {\n\tListOptions,\n\tListResult,\n\tObjectStat,\n\tObjectStore,\n} from \"../types.js\";\n\n/**\n * In-memory {@link ObjectStore}. Useful for tests, examples, and ephemeral\n * repositories; also the reference implementation for the list/delimiter\n * semantics other stores must match.\n */\nexport class MemoryObjectStore implements ObjectStore {\n\tprivate readonly objects = new Map<string, Uint8Array>();\n\n\tasync get(key: string): Promise<Uint8Array | null> {\n\t\tconst data = this.objects.get(key);\n\t\treturn data ? data.slice() : null;\n\t}\n\n\tasync getRange(\n\t\tkey: string,\n\t\tstart: number,\n\t\tend: number,\n\t): Promise<Uint8Array | null> {\n\t\tconst data = this.objects.get(key);\n\t\treturn data ? data.slice(start, end + 1) : null;\n\t}\n\n\tasync put(key: string, data: Uint8Array): Promise<void> {\n\t\tthis.objects.set(key, data.slice());\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tthis.objects.delete(key);\n\t}\n\n\tasync head(key: string): Promise<ObjectStat | null> {\n\t\tconst data = this.objects.get(key);\n\t\treturn data ? { size: data.byteLength } : null;\n\t}\n\n\tasync list(prefix: string, options?: ListOptions): Promise<ListResult> {\n\t\tconst delimiter = options?.delimiter;\n\t\tconst limit = options?.limit ?? Number.POSITIVE_INFINITY;\n\t\tconst objects: ListResult[\"objects\"] = [];\n\t\tconst prefixes = new Set<string>();\n\n\t\tfor (const [key, data] of this.objects) {\n\t\t\tif (!key.startsWith(prefix)) continue;\n\t\t\tconst rest = key.slice(prefix.length);\n\t\t\tif (delimiter !== undefined) {\n\t\t\t\tconst idx = rest.indexOf(delimiter);\n\t\t\t\tif (idx !== -1) {\n\t\t\t\t\tprefixes.add(prefix + rest.slice(0, idx + delimiter.length));\n\t\t\t\t} else {\n\t\t\t\t\tobjects.push({ key, size: data.byteLength });\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tobjects.push({ key, size: data.byteLength });\n\t\t\t}\n\t\t\tif (objects.length + prefixes.size >= limit) break;\n\t\t}\n\n\t\treturn { objects, prefixes: [...prefixes] };\n\t}\n\n\t/** Number of stored objects (test convenience, not part of ObjectStore). */\n\tget size(): number {\n\t\treturn this.objects.size;\n\t}\n}\n"],"mappings":";AAYO,IAAM,oBAAN,MAA+C;AAAA,EACpC,UAAU,oBAAI,IAAwB;AAAA,EAEvD,MAAM,IAAI,KAAyC;AAClD,UAAM,OAAO,KAAK,QAAQ,IAAI,GAAG;AACjC,WAAO,OAAO,KAAK,MAAM,IAAI;AAAA,EAC9B;AAAA,EAEA,MAAM,SACL,KACA,OACA,KAC6B;AAC7B,UAAM,OAAO,KAAK,QAAQ,IAAI,GAAG;AACjC,WAAO,OAAO,KAAK,MAAM,OAAO,MAAM,CAAC,IAAI;AAAA,EAC5C;AAAA,EAEA,MAAM,IAAI,KAAa,MAAiC;AACvD,SAAK,QAAQ,IAAI,KAAK,KAAK,MAAM,CAAC;AAAA,EACnC;AAAA,EAEA,MAAM,OAAO,KAA4B;AACxC,SAAK,QAAQ,OAAO,GAAG;AAAA,EACxB;AAAA,EAEA,MAAM,KAAK,KAAyC;AACnD,UAAM,OAAO,KAAK,QAAQ,IAAI,GAAG;AACjC,WAAO,OAAO,EAAE,MAAM,KAAK,WAAW,IAAI;AAAA,EAC3C;AAAA,EAEA,MAAM,KAAK,QAAgB,SAA4C;AACtE,UAAM,YAAY,SAAS;AAC3B,UAAM,QAAQ,SAAS,SAAS,OAAO;AACvC,UAAM,UAAiC,CAAC;AACxC,UAAM,WAAW,oBAAI,IAAY;AAEjC,eAAW,CAAC,KAAK,IAAI,KAAK,KAAK,SAAS;AACvC,UAAI,CAAC,IAAI,WAAW,MAAM,EAAG;AAC7B,YAAM,OAAO,IAAI,MAAM,OAAO,MAAM;AACpC,UAAI,cAAc,QAAW;AAC5B,cAAM,MAAM,KAAK,QAAQ,SAAS;AAClC,YAAI,QAAQ,IAAI;AACf,mBAAS,IAAI,SAAS,KAAK,MAAM,GAAG,MAAM,UAAU,MAAM,CAAC;AAAA,QAC5D,OAAO;AACN,kBAAQ,KAAK,EAAE,KAAK,MAAM,KAAK,WAAW,CAAC;AAAA,QAC5C;AAAA,MACD,OAAO;AACN,gBAAQ,KAAK,EAAE,KAAK,MAAM,KAAK,WAAW,CAAC;AAAA,MAC5C;AACA,UAAI,QAAQ,SAAS,SAAS,QAAQ,MAAO;AAAA,IAC9C;AAEA,WAAO,EAAE,SAAS,UAAU,CAAC,GAAG,QAAQ,EAAE;AAAA,EAC3C;AAAA;AAAA,EAGA,IAAI,OAAe;AAClB,WAAO,KAAK,QAAQ;AAAA,EACrB;AACD;","names":[]}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
rawFs,
|
|
15
15
|
sha1,
|
|
16
16
|
toHex
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-OKBQISUC.js";
|
|
18
18
|
|
|
19
19
|
// src/ops/tree.ts
|
|
20
20
|
import git from "isomorphic-git";
|
|
@@ -595,4 +595,4 @@ export {
|
|
|
595
595
|
storePack,
|
|
596
596
|
createSemaphore
|
|
597
597
|
};
|
|
598
|
-
//# sourceMappingURL=chunk-
|
|
598
|
+
//# sourceMappingURL=chunk-IAKZRWBZ.js.map
|
|
@@ -207,6 +207,9 @@ function formatErrorResponse(error) {
|
|
|
207
207
|
}
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
|
+
function isNotFoundError(error) {
|
|
211
|
+
return error instanceof GitPathNotFoundError || error instanceof GitObjectNotFoundError || error instanceof GitRefNotFoundError || error instanceof GitRepositoryNotFoundError;
|
|
212
|
+
}
|
|
210
213
|
|
|
211
214
|
// src/pack-files.ts
|
|
212
215
|
var PACK_FILE_KINDS = [
|
|
@@ -411,6 +414,7 @@ export {
|
|
|
411
414
|
GitInvalidRequestError,
|
|
412
415
|
GitProtocolError,
|
|
413
416
|
formatErrorResponse,
|
|
417
|
+
isNotFoundError,
|
|
414
418
|
PACK_FILE_KINDS,
|
|
415
419
|
READABLE_PACK_FILE_KINDS,
|
|
416
420
|
parsePackFileName,
|
|
@@ -435,4 +439,4 @@ export {
|
|
|
435
439
|
isSafeTreePath,
|
|
436
440
|
qualifyBranchRef
|
|
437
441
|
};
|
|
438
|
-
//# sourceMappingURL=chunk-
|
|
442
|
+
//# sourceMappingURL=chunk-OKBQISUC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/edge-utils.ts","../src/git-errors.ts","../src/pack-files.ts","../src/http/types.ts","../src/refs.ts","../src/pack-entry.ts","../src/pack-index.ts"],"sourcesContent":["/**\n * Edge-compatible utilities replacing node:crypto, node:zlib, and Buffer.\n *\n * Every function here uses only Web APIs (SubtleCrypto, CompressionStream,\n * TextEncoder/TextDecoder) — no Node built-ins. They work on Cloudflare\n * Workers, Vercel Edge, Deno Deploy, and Node >= 18.\n */\n\nconst textEncoder = new TextEncoder();\nconst textDecoder = new TextDecoder();\n\n// ---------------------------------------------------------------------------\n// Text\n// ---------------------------------------------------------------------------\n\n/**\n * Encode a UTF-8 string to bytes.\n *\n * Return type pinned to `Uint8Array<ArrayBuffer>` (not the bare `Uint8Array`,\n * whose default type argument differs across TypeScript versions) so it's\n * always assignable to Fetch API `BodyInit` regardless of a consumer's own\n * TypeScript/lib version.\n */\nexport function encodeUtf8(data: string): Uint8Array<ArrayBuffer> {\n\treturn textEncoder.encode(data);\n}\n\n/** Decode bytes as UTF-8. */\nexport function decodeUtf8(data: Uint8Array): string {\n\treturn textDecoder.decode(data);\n}\n\n/** Decode bytes as ASCII. */\nexport function decodeAscii(data: Uint8Array): string {\n\tlet s = \"\";\n\tfor (let i = 0; i < data.length; i++)\n\t\ts += String.fromCharCode(data[i] as number);\n\treturn s;\n}\n\n// ---------------------------------------------------------------------------\n// Array manipulation\n// ---------------------------------------------------------------------------\n\n/** Concatenate any number of Uint8Arrays into one. */\nexport function concat(...parts: Uint8Array[]): Uint8Array<ArrayBuffer> {\n\tlet total = 0;\n\tfor (const p of parts) total += p.length;\n\tconst out = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const p of parts) {\n\t\tout.set(p, offset);\n\t\toffset += p.length;\n\t}\n\treturn out;\n}\n\n// ---------------------------------------------------------------------------\n// Encoding\n// ---------------------------------------------------------------------------\n\n/** Uint8Array → lowercase hex string. */\nexport function toHex(data: Uint8Array): string {\n\tlet hex = \"\";\n\tfor (let i = 0; i < data.length; i++)\n\t\thex += (data[i] as number).toString(16).padStart(2, \"0\");\n\treturn hex;\n}\n\n/** Uint8Array → base64 string. */\nexport function toBase64(data: Uint8Array): string {\n\tlet binary = \"\";\n\tfor (let i = 0; i < data.length; i++)\n\t\tbinary += String.fromCharCode(data[i] as number);\n\treturn btoa(binary);\n}\n\n/** Hex string → Uint8Array. */\nexport function fromHex(hex: string): Uint8Array<ArrayBuffer> {\n\tconst bytes = new Uint8Array(hex.length / 2);\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\tbytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);\n\t}\n\treturn bytes;\n}\n\n// ---------------------------------------------------------------------------\n// Crypto\n// ---------------------------------------------------------------------------\n\n/** SHA-1 hash via Web Crypto API. Returns a hex string. */\nexport async function sha1(\n\tdata: Uint8Array<ArrayBuffer> | string,\n): Promise<string> {\n\tconst bytes = typeof data === \"string\" ? encodeUtf8(data) : data;\n\tconst hash = await globalThis.crypto.subtle.digest(\"SHA-1\", bytes);\n\treturn toHex(new Uint8Array(hash));\n}\n\n// ---------------------------------------------------------------------------\n// Compression\n// ---------------------------------------------------------------------------\n\n/** Deflate (zlib) via the CompressionStream Web API. */\nexport async function deflate(\n\tdata: Uint8Array<ArrayBuffer>,\n): Promise<Uint8Array<ArrayBuffer>> {\n\t// Response, not Blob.stream(): some Blob implementations (jsdom's) have\n\t// no stream().\n\tconst stream = (new Response(data).body as ReadableStream).pipeThrough(\n\t\tnew CompressionStream(\"deflate\"),\n\t);\n\treturn new Uint8Array(await new Response(stream).arrayBuffer());\n}\n\n/**\n * The first `enough` bytes of a deflate stream, given a prefix of that stream.\n *\n * Deflate emits output as it consumes input, so a bounded compressed prefix\n * yields a bounded uncompressed prefix — which is how a delta entry's header\n * (the only place its reconstructed size is recorded) can be read without\n * inflating the whole object. The prefix ends mid-symbol by construction, so\n * the stream errors after the bytes we wanted have already arrived; that error\n * carries no information and is not propagated.\n *\n * Returns `null` when fewer than `enough` bytes could be recovered, so a caller\n * distinguishes \"not readable from this prefix\" from a zero-length answer.\n */\nexport async function inflatePrefix(\n\tcompressed: Uint8Array<ArrayBuffer>,\n\tenough: number,\n): Promise<Uint8Array<ArrayBuffer> | null> {\n\tconst stream = (new Response(compressed).body as ReadableStream).pipeThrough(\n\t\tnew DecompressionStream(\"deflate\"),\n\t);\n\tconst reader = stream.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\ttry {\n\t\twhile (total < enough) {\n\t\t\tconst { done, value } = await reader.read();\n\t\t\tif (done) break;\n\t\t\tchunks.push(value);\n\t\t\ttotal += value.byteLength;\n\t\t}\n\t} catch {\n\t\t// Expected: the compressed prefix is truncated. Whatever was emitted\n\t\t// before that is what this returns.\n\t} finally {\n\t\tawait reader.cancel().catch(() => {});\n\t}\n\tif (total === 0) return null;\n\treturn concat(...chunks.map((chunk) => new Uint8Array(chunk)));\n}\n\n/** Inflate (zlib) via the DecompressionStream Web API. */\nexport async function inflate(\n\tdata: Uint8Array<ArrayBuffer>,\n): Promise<Uint8Array<ArrayBuffer>> {\n\tconst stream = (new Response(data).body as ReadableStream).pipeThrough(\n\t\tnew DecompressionStream(\"deflate\"),\n\t);\n\treturn new Uint8Array(await new Response(stream).arrayBuffer());\n}\n\n// ---------------------------------------------------------------------------\n// Binary detection (replaces Buffer.includes(0) pattern)\n// ---------------------------------------------------------------------------\n\n/** Check if a Uint8Array contains a null byte. */\nexport function hasNullByte(data: Uint8Array): boolean {\n\treturn data.includes(0);\n}\n\n/**\n * Read a blob as text or binary metadata — the edge-compatible replacement\n * for the `Buffer.from(blob)` pattern used throughout diff.ts and history.ts.\n */\nexport function readBlobContent(blob: Uint8Array): {\n\tisBinary: boolean;\n\ttext: string;\n\tbytes: Uint8Array;\n} {\n\tconst isBinary = hasNullByte(blob);\n\treturn {\n\t\tisBinary,\n\t\ttext: isBinary ? \"\" : decodeUtf8(blob),\n\t\tbytes: blob,\n\t};\n}\n","/**\n * Git-server error types carrying an HTTP status and a retryability flag, so\n * transport layers can map internal failures to responses without inspecting\n * messages. Extend {@link GitError} for app-specific cases (storage backends,\n * quota, …) and {@link formatErrorResponse} keeps working for them.\n */\nexport class GitError extends Error {\n\tstatusCode: number;\n\tretryable: boolean;\n\n\tconstructor(message: string, statusCode = 500, retryable = false) {\n\t\tsuper(message);\n\t\tthis.name = this.constructor.name;\n\t\tthis.statusCode = statusCode;\n\t\tthis.retryable = retryable;\n\t\tError.captureStackTrace?.(this, this.constructor);\n\t}\n\n\ttoJSON(): Record<string, unknown> {\n\t\treturn {\n\t\t\terror: this.name,\n\t\t\tmessage: this.message,\n\t\t\tstatusCode: this.statusCode,\n\t\t\tretryable: this.retryable,\n\t\t};\n\t}\n}\n\n/** A file/directory path not found within a tree (404). */\nexport class GitPathNotFoundError extends GitError {\n\tconstructor(message: string) {\n\t\tsuper(message, 404, false);\n\t}\n}\n\n/** A git object not found (404). */\nexport class GitObjectNotFoundError extends GitError {\n\tconstructor(message: string) {\n\t\tsuper(message, 404, false);\n\t}\n}\n\n/** A ref (branch/tag) not found (404). */\nexport class GitRefNotFoundError extends GitError {\n\tconstructor(message: string) {\n\t\tsuper(message, 404, false);\n\t}\n}\n\n/** The repository itself not found (404). */\nexport class GitRepositoryNotFoundError extends GitError {\n\tconstructor(message: string) {\n\t\tsuper(message, 404, false);\n\t}\n}\n\nexport interface MergeConflictDetail {\n\tfile: string;\n\tbaseLines?: string[];\n\tsourceLines?: string[];\n\ttargetLines?: string[];\n}\n\n/** A merge conflict (409), carrying per-file conflict detail. */\nexport class GitConflictError extends GitError {\n\tconflicts: MergeConflictDetail[];\n\n\tconstructor(message: string, conflicts: MergeConflictDetail[] = []) {\n\t\tsuper(message, 409, false);\n\t\tthis.conflicts = conflicts;\n\t}\n\n\toverride toJSON(): Record<string, unknown> {\n\t\treturn { ...super.toJSON(), conflicts: this.conflicts };\n\t}\n}\n\n/** Authentication failed (401). */\nexport class GitAuthenticationError extends GitError {\n\tconstructor(message: string) {\n\t\tsuper(message, 401, false);\n\t}\n}\n\n/** Authorization failed (403). */\nexport class GitAuthorizationError extends GitError {\n\tconstructor(message: string) {\n\t\tsuper(message, 403, false);\n\t}\n}\n\n/** Too many failed attempts (429). */\nexport class GitRateLimitError extends GitError {\n\tconstructor(message: string) {\n\t\tsuper(message, 429, false);\n\t}\n}\n\n/** Malformed request (400). */\nexport class GitInvalidRequestError extends GitError {\n\tconstructor(message: string) {\n\t\tsuper(message, 400, false);\n\t}\n}\n\n/** Git wire-protocol violation (400). */\nexport class GitProtocolError extends GitError {\n\tconstructor(message: string) {\n\t\tsuper(message, 400, false);\n\t}\n}\n\n/**\n * Map any error to an HTTP response shape. 401s carry the WWW-Authenticate\n * header git clients need before they will prompt for credentials. Non-GitError\n * failures are masked as opaque 500s — internal messages don't leak.\n */\nexport function formatErrorResponse(error: unknown): {\n\tstatus: number;\n\tbody: Record<string, unknown>;\n\theaders?: Record<string, string>;\n} {\n\tif (error instanceof GitError) {\n\t\treturn {\n\t\t\tstatus: error.statusCode,\n\t\t\tbody: error.toJSON(),\n\t\t\theaders:\n\t\t\t\terror.statusCode === 401\n\t\t\t\t\t? { \"WWW-Authenticate\": 'Basic realm=\"Git Repository\"' }\n\t\t\t\t\t: undefined,\n\t\t};\n\t}\n\n\t// The object store's circuit breaker is open (retry.ts's CircuitOpenError):\n\t// the backend is down, not this request — tell clients when to retry.\n\tif ((error as { code?: unknown } | null)?.code === \"EUNAVAILABLE\") {\n\t\treturn {\n\t\t\tstatus: 503,\n\t\t\tbody: {\n\t\t\t\terror: \"ServiceUnavailable\",\n\t\t\t\tmessage: \"Storage is temporarily unavailable\",\n\t\t\t\tretryable: true,\n\t\t\t},\n\t\t\theaders: { \"Retry-After\": \"30\" },\n\t\t};\n\t}\n\n\tif (error instanceof Error) {\n\t\treturn {\n\t\t\tstatus: 500,\n\t\t\tbody: {\n\t\t\t\terror: \"InternalServerError\",\n\t\t\t\tmessage: \"An internal error occurred\",\n\t\t\t\tretryable: true,\n\t\t\t},\n\t\t};\n\t}\n\n\treturn {\n\t\tstatus: 500,\n\t\tbody: {\n\t\t\terror: \"UnknownError\",\n\t\t\tmessage: \"An unknown error occurred\",\n\t\t\tretryable: true,\n\t\t},\n\t};\n}\n\n/** A missing repository, ref, path or object: on a read path that's a 404. */\nexport function isNotFoundError(error: unknown): boolean {\n\treturn (\n\t\terror instanceof GitPathNotFoundError ||\n\t\terror instanceof GitObjectNotFoundError ||\n\t\terror instanceof GitRefNotFoundError ||\n\t\terror instanceof GitRepositoryNotFoundError\n\t);\n}\n","/**\n * Typed view of a git `objects/pack` directory listing.\n *\n * Every pack is a set of sibling files sharing one base name\n * (`pack-<hash>.pack`, `.idx`, and optionally `.keep`/`.bitmap`/`.rev`).\n * Parsing names once into {@link PackFileName} lets callers group, filter and\n * rename by kind instead of re-matching filename strings at each call site.\n *\n * Edge-compatible: no node: imports.\n */\n\n/** Every file kind git writes into `objects/pack`. */\nexport const PACK_FILE_KINDS = [\n\t\"pack\",\n\t\"idx\",\n\t\"keep\",\n\t\"bitmap\",\n\t\"rev\",\n] as const;\nexport type PackFileKind = (typeof PACK_FILE_KINDS)[number];\n\n/** Kinds isomorphic-git actually reads; the rest are git-CLI accelerators. */\nexport const READABLE_PACK_FILE_KINDS: ReadonlySet<PackFileKind> = new Set([\n\t\"pack\",\n\t\"idx\",\n]);\n\nexport interface PackFileName {\n\t/** Shared base of a pack's sibling files, e.g. `pack-1a2b…`. */\n\treadonly base: string;\n\treadonly kind: PackFileKind;\n\t/** The file name as listed, e.g. `pack-1a2b….idx`. */\n\treadonly name: string;\n}\n\nconst KNOWN_KINDS: ReadonlySet<string> = new Set(PACK_FILE_KINDS);\n\nfunction isPackFileKind(value: string): value is PackFileKind {\n\treturn KNOWN_KINDS.has(value);\n}\n\n/** Parse a pack-directory entry; `null` for anything git didn't put there. */\nexport function parsePackFileName(name: string): PackFileName | null {\n\tconst dot = name.lastIndexOf(\".\");\n\tif (dot <= 0) return null;\n\tconst kind = name.slice(dot + 1);\n\tif (!isPackFileKind(kind)) return null;\n\treturn { base: name.slice(0, dot), kind, name };\n}\n\n/** File name of the `kind` sibling of the pack with this base name. */\nexport function packFileName(base: string, kind: PackFileKind): string {\n\treturn `${base}.${kind}`;\n}\n\n/** Parse a directory listing, dropping entries that aren't pack files. */\nexport function parsePackDirectory(entries: readonly string[]): PackFileName[] {\n\treturn entries\n\t\t.map(parsePackFileName)\n\t\t.filter((file): file is PackFileName => file !== null);\n}\n","import type { Repo } from \"../ops/types.js\";\n\n/**\n * Transport-agnostic HTTP response the handlers produce.\n *\n * `body` is pinned to `Uint8Array<ArrayBuffer>` (not the bare `Uint8Array`,\n * whose default type argument isn't consistent across TypeScript versions)\n * so it's always assignable to Fetch API `BodyInit` regardless of which\n * TypeScript/lib version a consumer compiles against — every producer here\n * (`concat`, `pktLine`, `TextEncoder.encode`) is ArrayBuffer-backed already.\n */\nexport interface GitHttpResult {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: Uint8Array<ArrayBuffer>;\n}\n\n/** Optional instrumentation hooks shared by the /http handlers. */\nexport interface HttpHooks {\n\t/** Wrap a timed sub-step. Default: run directly. */\n\tstep?: <T>(label: string, fn: () => Promise<T>) => Promise<T>;\n\t/** Non-fatal problem sink (missing objects, failed repacks). */\n\tonWarn?: (message: string, error?: unknown) => void;\n}\n\nexport const runStep = <T>(\n\thooks: HttpHooks | undefined,\n\tlabel: string,\n\tfn: () => Promise<T>,\n): Promise<T> => (hooks?.step ? hooks.step(label, fn) : fn());\n\n/**\n * The raw promise-fs surface some /http paths need beyond isomorphic-git's\n * plumbing (writing an incoming pack file, listing/deleting pack files).\n * Both `node:fs` and this package's `GitFs` satisfy it via `.promises`.\n */\nexport interface RawFsPromises {\n\treadFile(path: string): Promise<Uint8Array | string>;\n\twriteFile(path: string, data: Uint8Array | string): Promise<void>;\n\tunlink(path: string): Promise<void>;\n\treaddir(path: string): Promise<string[]>;\n\tmkdir(path: string, options?: { recursive?: boolean }): Promise<unknown>;\n\tstat(path: string): Promise<unknown>;\n}\n\n/** Duck-type the `.promises` surface off a repo's fs. */\nexport function rawFs(repo: Repo): RawFsPromises {\n\tconst fs = repo.fs as { promises?: RawFsPromises };\n\tif (!fs?.promises) {\n\t\tthrow new TypeError(\n\t\t\t\"This operation needs a promise fs (`fs.promises`) — node:fs and createGitFs both provide one\",\n\t\t);\n\t}\n\treturn fs.promises;\n}\n","/**\n * Git ref-name validation, mirroring isomorphic-git's own internal `isValidRef`\n * character-class rules (the check `git.branch` and top-level `git.writeRef`\n * run before touching disk).\n *\n * Several of isomorphic-git's OTHER ref-touching primitives — `git.commit`,\n * `git.merge`, `git.deleteBranch`, and top-level `git.resolveRef`/\n * `git.deleteRef` — do NOT run this check internally: they resolve straight\n * through `fs.write`/`fs.rm(join(gitdir, ref))` with no jail to the gitdir.\n * On a shared-storage server (many repos under one prefix or base directory),\n * every branch/ref name that originates from request input must be validated\n * against these predicates before it reaches any of those primitives —\n * otherwise a `\"../\"`-laden name lets a caller with write access to any single\n * repo read, corrupt, or delete another repo's ref/object files.\n */\n\nconst BAD_REF_COMPONENT =\n\t// biome-ignore lint/suspicious/noControlCharactersInRegex: control chars are exactly what git's own ref-name rules reject — this needs to match the same range.\n\t/(^|[/.])([/.]|$)|^@$|@\\{|[\\x00-\\x20\\x7f~^:?*[\\\\]|\\.lock(\\/|$)/;\n\nconst FULL_SHA_RE = /^[0-9a-f]{40}$/i;\n\n/** Validates a fully-qualified ref (must start with refs/heads/ or refs/tags/). */\nexport function isSafeFullRefName(ref: string): boolean {\n\tif (!ref.startsWith(\"refs/heads/\") && !ref.startsWith(\"refs/tags/\")) {\n\t\treturn false;\n\t}\n\treturn !BAD_REF_COMPONENT.test(ref);\n}\n\n/**\n * Validates a bare branch name (no refs/ prefix). Rejects anything that looks\n * like a full ref path — a name of `\"refs/heads/x\"` would otherwise sail\n * through unprefixed at call sites that build `refs/heads/${name}` themselves\n * (doubling the prefix into something that still resolves), or be used as-is\n * at call sites that pass a name already containing `\"refs/\"` straight\n * through. Also rejects 40-hex SHA-shaped values so a stored branch name can\n * never be ambiguous with a commit SHA at write time; use\n * {@link isSafeRefName} on read paths that accept both shapes.\n */\nexport function isSafeBranchName(name: string): boolean {\n\tif (!name || name.startsWith(\"refs/\") || name === \"HEAD\") return false;\n\tif (FULL_SHA_RE.test(name)) return false;\n\treturn !BAD_REF_COMPONENT.test(name);\n}\n\n/** True for a full 40-hex-char commit SHA — the shape {@link isSafeBranchName} deliberately rejects. */\nexport function isFullSha(value: string): boolean {\n\treturn FULL_SHA_RE.test(value);\n}\n\n/**\n * Validates a \"ref\" field that may name either a branch or a commit SHA it's\n * pinned to — the shape read-path route params take (permalinks, raw links).\n * Both shapes still go through the traversal check.\n */\nexport function isSafeRefName(value: string): boolean {\n\treturn isSafeBranchName(value) || isFullSha(value);\n}\n\n/**\n * Validates a repo-relative file path from request input: relative, no `..`\n * segments, no `.git/` prefix, no null bytes. Use this anywhere a path\n * segment comes straight off a URL or form field rather than re-deriving the\n * checks ad hoc.\n */\nexport function isSafeRepoPath(p: string): boolean {\n\tif (p.startsWith(\"/\")) return false;\n\tif (p.split(\"/\").some((segment) => segment === \"..\")) return false;\n\tif (/^\\.git(\\/|$)/i.test(p)) return false;\n\tif (p.includes(\"\\0\")) return false;\n\treturn true;\n}\n\n/**\n * Whether `p` can name a file written into a tree: what {@link isSafeRepoPath}\n * checks, plus every segment non-empty and neither `.` nor `..`, and no\n * `.git` segment anywhere (any case) — git itself refuses to check those out,\n * since a tracked `.git/config` in a subdirectory is a code-execution vector\n * on clone. Use it for paths that become tree entries; reads keep the looser\n * {@link isSafeRepoPath} (which also accepts `\"\"`, the root).\n */\nexport function isSafeTreePath(p: string): boolean {\n\tif (!isSafeRepoPath(p) || p.length === 0) return false;\n\treturn p\n\t\t.split(\"/\")\n\t\t.every(\n\t\t\t(segment) =>\n\t\t\t\tsegment.length > 0 &&\n\t\t\t\tsegment !== \".\" &&\n\t\t\t\tsegment !== \"..\" &&\n\t\t\t\tsegment.toLowerCase() !== \".git\",\n\t\t);\n}\n\n/**\n * Qualify a bare branch name to `refs/heads/<name>` before handing it to\n * isomorphic-git. `resolveRef`/`expand` try several candidate paths in\n * sequence for a bare name — `ref`, `refs/ref`, `refs/tags/ref`,\n * `refs/heads/ref`, … — missing (and, against object storage, paying a real\n * round trip for) the first three every time. For a branch-only ref model,\n * skip straight to the winner. Left untouched: already-qualified refs,\n * `\"HEAD\"` (its own first candidate, already optimal), and 40-hex oids\n * (resolved locally by isomorphic-git with no I/O at all).\n */\nexport function qualifyBranchRef(ref: string): string {\n\tif (ref.startsWith(\"refs/\") || ref === \"HEAD\" || FULL_SHA_RE.test(ref)) {\n\t\treturn ref;\n\t}\n\treturn `refs/heads/${ref}`;\n}\n","/**\n * Parsing the entry headers inside a git packfile (`gitformat-pack(5)`).\n *\n * Shared by the two places that read a pack's structure rather than its\n * objects: `http/pack-reuse.ts` (copying stored entries into a new pack) and\n * `object-size.ts` (answering an object's size without inflating it).\n *\n * Edge-compatible: `Uint8Array` only.\n */\n\n/** Git pack object type bits (bits 6-4 of an entry's first byte). */\nexport const OBJ_COMMIT = 1;\nexport const OBJ_TREE = 2;\nexport const OBJ_BLOB = 3;\nexport const OBJ_TAG = 4;\nexport const OBJ_OFS_DELTA = 6;\nexport const OBJ_REF_DELTA = 7;\n\n/** Bytes in a pack object's id, and in a `REF_DELTA`'s base reference. */\nexport const OID_BYTES = 20;\n\n/** The variable-length type/size header that starts every pack entry. */\nexport interface EntryHeader {\n\treadonly type: number;\n\t/**\n\t * For a whole object, its size in bytes. For a delta entry, the size of the\n\t * **delta stream**, not of the object it reconstructs — that is in the delta\n\t * header (see {@link readDeltaHeader}).\n\t */\n\treadonly size: number;\n\t/** Bytes the header occupies. */\n\treadonly length: number;\n}\n\n/**\n * Read the entry header at `start`.\n *\n * A header that runs off the end of `bytes` yields whatever digits were\n * available: this reads from a bounded slice of a pack, and every caller\n * decides for itself whether the result is plausible (`length` beyond what it\n * read means \"unknown\", not \"zero\").\n */\nexport function readEntryHeader(bytes: Uint8Array, start: number): EntryHeader {\n\tlet byte = bytes[start] as number;\n\tconst type = (byte >> 4) & 0b111;\n\tlet size = byte & 0b1111;\n\tlet shift = 4;\n\tlet at = start + 1;\n\twhile (byte & 0x80) {\n\t\tbyte = bytes[at++] as number;\n\t\tsize += (byte & 0x7f) * 2 ** shift;\n\t\tshift += 7;\n\t}\n\treturn { type, size, length: at - start };\n}\n\n/** Serialize an entry header (the inverse of {@link readEntryHeader}). */\nexport function encodeEntryHeader(type: number, size: number): Uint8Array {\n\tconst bytes: number[] = [];\n\tlet rest = Math.floor(size / 16);\n\tbytes.push((rest > 0 ? 0x80 : 0) | (type << 4) | (size % 16));\n\twhile (rest > 0) {\n\t\tconst low = rest % 128;\n\t\trest = Math.floor(rest / 128);\n\t\tbytes.push((rest > 0 ? 0x80 : 0) | low);\n\t}\n\treturn new Uint8Array(bytes);\n}\n\n/** `OFS_DELTA`'s negative base offset: big-endian base-128 with +1 carries. */\nexport function readOfsDeltaOffset(\n\tbytes: Uint8Array,\n\tstart: number,\n): { distance: number; length: number } {\n\tlet at = start;\n\tlet byte = bytes[at++] as number;\n\tlet distance = byte & 0x7f;\n\twhile (byte & 0x80) {\n\t\tbyte = bytes[at++] as number;\n\t\tdistance = (distance + 1) * 128 + (byte & 0x7f);\n\t}\n\treturn { distance, length: at - start };\n}\n\n/** The two varints a delta stream opens with: base size, then result size. */\nexport interface DeltaHeader {\n\t/** Size of the object the delta is written against. */\n\treadonly baseSize: number;\n\t/** Size of the object the delta reconstructs. */\n\treadonly resultSize: number;\n\t/** Bytes the two varints occupy. */\n\treadonly length: number;\n}\n\n/** Read the base-128 little-endian varint at `start`, 7 bits per byte. */\nfunction readVarint(\n\tbytes: Uint8Array,\n\tstart: number,\n): { value: number; length: number } {\n\tlet at = start;\n\tlet byte = bytes[at++] as number;\n\tlet value = byte & 0x7f;\n\tlet shift = 7;\n\twhile (byte & 0x80) {\n\t\tbyte = bytes[at++] as number;\n\t\tvalue += (byte & 0x7f) * 2 ** shift;\n\t\tshift += 7;\n\t}\n\treturn { value, length: at - start };\n}\n\n/**\n * Read the delta header that follows an `OFS_DELTA` / `REF_DELTA` entry's own\n * header (and, for `REF_DELTA`, its 20-byte base id): the size of the object\n * being modified, then the size of the result. The result size is the answer\n * {@link ObjectSize} exists to get — it is the only place a deltified object's\n * real length is recorded.\n */\nexport function readDeltaHeader(bytes: Uint8Array, start: number): DeltaHeader {\n\tconst base = readVarint(bytes, start);\n\tconst result = readVarint(bytes, start + base.length);\n\treturn {\n\t\tbaseSize: base.value,\n\t\tresultSize: result.value,\n\t\tlength: base.length + result.length,\n\t};\n}\n","/**\n * Reading a pack's version-2 `.idx` (`gitformat-pack(5)`) — the oid → offset\n * table git writes beside every packfile.\n *\n * Kept separate from the code that reads pack *entries*: an index lookup is how\n * you find out which bytes to look at, and it is the cheap half of any question\n * about a packed object's contents (`http/pack-reuse.ts` copies entries into a\n * new pack; `object-size.ts` reads one entry's header).\n *\n * Edge-compatible: `Uint8Array` and `DataView` only.\n */\n\nimport { toHex } from \"./edge-utils.js\";\nimport { OID_BYTES } from \"./pack-entry.js\";\n\nconst IDX_V2 = {\n\tmagic: 0xff744f63,\n\tversion: 2,\n\theaderBytes: 8,\n\tfanoutEntries: 256,\n\t/** Offsets with this bit set index the 64-bit large-offset table. */\n\tlargeOffsetFlag: 0x80000000,\n} as const;\n\n/** An object's position in a pack, as listed by its `.idx`. */\nexport interface PackIndexEntry {\n\treadonly oid: string;\n\treadonly offset: number;\n}\n\n/**\n * Entries of a version-2 `.idx`, in the index's own (oid) order; `null` for any\n * other format, or for one whose declared object count doesn't fit the bytes\n * present — an index that fails either check is not something to guess about.\n */\nexport function readPackIndex(idx: Uint8Array): PackIndexEntry[] | null {\n\tconst view = new DataView(idx.buffer, idx.byteOffset, idx.byteLength);\n\tconst namesStart = IDX_V2.headerBytes + IDX_V2.fanoutEntries * 4;\n\tif (\n\t\tidx.byteLength < namesStart ||\n\t\tview.getUint32(0) !== IDX_V2.magic ||\n\t\tview.getUint32(4) !== IDX_V2.version\n\t) {\n\t\treturn null;\n\t}\n\t// The last fanout slot counts every object in the pack.\n\tconst count = view.getUint32(namesStart - 4);\n\tconst crcStart = namesStart + count * OID_BYTES;\n\tconst offsetsStart = crcStart + count * 4;\n\tif (idx.byteLength < offsetsStart + count * 4) return null;\n\n\tconst entries: PackIndexEntry[] = [];\n\tfor (let i = 0; i < count; i++) {\n\t\tconst offset = view.getUint32(offsetsStart + i * 4);\n\t\t// Packs over 2 GiB use the 64-bit offset table instead; callers that\n\t\t// can't handle it treat this as \"not readable\" rather than guessing.\n\t\tif (offset & IDX_V2.largeOffsetFlag) return null;\n\t\tconst at = namesStart + i * OID_BYTES;\n\t\tentries.push({ oid: toHex(idx.subarray(at, at + OID_BYTES)), offset });\n\t}\n\treturn entries;\n}\n\n/** Offset of `oid` in `idx`, or `null` when the index doesn't list it. */\nexport function findPackIndexOffset(\n\tidx: Uint8Array,\n\toid: string,\n): number | null {\n\treturn readPackIndex(idx)?.find((entry) => entry.oid === oid)?.offset ?? null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
readDeltaHeader,
|
|
12
12
|
readEntryHeader,
|
|
13
13
|
readOfsDeltaOffset
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-OKBQISUC.js";
|
|
15
15
|
|
|
16
16
|
// src/git-layout.ts
|
|
17
17
|
var HEX = new Set("0123456789abcdef");
|
|
@@ -140,4 +140,4 @@ export {
|
|
|
140
140
|
parseLooseObjectPath,
|
|
141
141
|
getObjectSize
|
|
142
142
|
};
|
|
143
|
-
//# sourceMappingURL=chunk-
|
|
143
|
+
//# sourceMappingURL=chunk-T5M5EYCZ.js.map
|
package/dist/fetch-s3.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// src/stores/fetch-s3.ts
|
|
2
|
+
import { AwsClient } from "aws4fetch";
|
|
3
|
+
var S3HttpError = class extends Error {
|
|
4
|
+
$metadata;
|
|
5
|
+
constructor(operation, key, status, detail) {
|
|
6
|
+
super(
|
|
7
|
+
`S3 ${operation} ${key} failed: HTTP ${status}${detail ? ` \u2014 ${detail}` : ""}`
|
|
8
|
+
);
|
|
9
|
+
this.name = status === 429 ? "SlowDown" : "S3HttpError";
|
|
10
|
+
this.$metadata = { httpStatusCode: status };
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
function encodeKey(key) {
|
|
14
|
+
return key.split("/").map(encodeURIComponent).join("/");
|
|
15
|
+
}
|
|
16
|
+
function decodeXml(text) {
|
|
17
|
+
return text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(
|
|
18
|
+
/&#(\d+);/g,
|
|
19
|
+
(_, code) => String.fromCodePoint(Number(code))
|
|
20
|
+
).replace(
|
|
21
|
+
/&#x([0-9a-f]+);/gi,
|
|
22
|
+
(_, code) => String.fromCodePoint(Number.parseInt(code, 16))
|
|
23
|
+
).replace(/&/g, "&");
|
|
24
|
+
}
|
|
25
|
+
function xmlValues(xml, tag) {
|
|
26
|
+
const pattern = new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, "g");
|
|
27
|
+
return [...xml.matchAll(pattern)].map((match) => decodeXml(match[1] ?? ""));
|
|
28
|
+
}
|
|
29
|
+
function parseListObjectsV2(xml) {
|
|
30
|
+
const objects = xmlValues(xml, "Contents").map((entry) => ({
|
|
31
|
+
key: xmlValues(entry, "Key")[0] ?? "",
|
|
32
|
+
size: Number(xmlValues(entry, "Size")[0] ?? 0)
|
|
33
|
+
}));
|
|
34
|
+
const prefixes = xmlValues(xml, "CommonPrefixes").flatMap(
|
|
35
|
+
(entry) => xmlValues(entry, "Prefix")
|
|
36
|
+
);
|
|
37
|
+
const truncated = xmlValues(xml, "IsTruncated")[0] === "true";
|
|
38
|
+
const nextToken = truncated ? xmlValues(xml, "NextContinuationToken")[0] : void 0;
|
|
39
|
+
return { objects, prefixes, nextToken };
|
|
40
|
+
}
|
|
41
|
+
function createFetchS3Store(options) {
|
|
42
|
+
const BODY_TIMEOUT_MS = options.bodyTimeoutMs ?? 3e4;
|
|
43
|
+
const METADATA_TIMEOUT_MS = options.metadataTimeoutMs ?? 1e4;
|
|
44
|
+
const client = new AwsClient({
|
|
45
|
+
accessKeyId: options.accessKeyId,
|
|
46
|
+
secretAccessKey: options.secretAccessKey,
|
|
47
|
+
service: "s3",
|
|
48
|
+
region: options.region ?? "auto",
|
|
49
|
+
// createRetryStore owns retries; aws4fetch's own would stack under it.
|
|
50
|
+
retries: 0
|
|
51
|
+
});
|
|
52
|
+
const doFetch = options.fetch ?? fetch;
|
|
53
|
+
const bucketUrl = `${options.endpoint.replace(/\/+$/, "")}/${encodeURIComponent(options.bucket)}`;
|
|
54
|
+
async function send(url, init, timeoutMs) {
|
|
55
|
+
const request = await client.sign(url, init);
|
|
56
|
+
return doFetch(request, { signal: AbortSignal.timeout(timeoutMs) });
|
|
57
|
+
}
|
|
58
|
+
async function fail(operation, key, response) {
|
|
59
|
+
const detail = (await response.text().catch(() => "")).slice(0, 300);
|
|
60
|
+
throw new S3HttpError(operation, key, response.status, detail);
|
|
61
|
+
}
|
|
62
|
+
const objectUrl = (key) => `${bucketUrl}/${encodeKey(key)}`;
|
|
63
|
+
return {
|
|
64
|
+
async get(key) {
|
|
65
|
+
const response = await send(
|
|
66
|
+
objectUrl(key),
|
|
67
|
+
{ method: "GET" },
|
|
68
|
+
BODY_TIMEOUT_MS
|
|
69
|
+
);
|
|
70
|
+
if (response.status === 404) {
|
|
71
|
+
await response.body?.cancel();
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (!response.ok) return fail("GET", key, response);
|
|
75
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
76
|
+
},
|
|
77
|
+
async getRange(key, start, end) {
|
|
78
|
+
const response = await send(
|
|
79
|
+
objectUrl(key),
|
|
80
|
+
{ method: "GET", headers: { Range: `bytes=${start}-${end}` } },
|
|
81
|
+
METADATA_TIMEOUT_MS
|
|
82
|
+
);
|
|
83
|
+
if (response.status === 404) {
|
|
84
|
+
await response.body?.cancel();
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (!response.ok) return fail("GET range", key, response);
|
|
88
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
89
|
+
},
|
|
90
|
+
async put(key, data) {
|
|
91
|
+
const headers = {};
|
|
92
|
+
const type = options.contentType?.(key);
|
|
93
|
+
if (type) headers["Content-Type"] = type;
|
|
94
|
+
const response = await send(
|
|
95
|
+
objectUrl(key),
|
|
96
|
+
{ method: "PUT", headers, body: data },
|
|
97
|
+
BODY_TIMEOUT_MS
|
|
98
|
+
);
|
|
99
|
+
if (!response.ok) return fail("PUT", key, response);
|
|
100
|
+
await response.body?.cancel();
|
|
101
|
+
},
|
|
102
|
+
async delete(key) {
|
|
103
|
+
const response = await send(
|
|
104
|
+
objectUrl(key),
|
|
105
|
+
{ method: "DELETE" },
|
|
106
|
+
METADATA_TIMEOUT_MS
|
|
107
|
+
);
|
|
108
|
+
if (!response.ok && response.status !== 404) {
|
|
109
|
+
return fail("DELETE", key, response);
|
|
110
|
+
}
|
|
111
|
+
await response.body?.cancel();
|
|
112
|
+
},
|
|
113
|
+
async head(key) {
|
|
114
|
+
const response = await send(
|
|
115
|
+
objectUrl(key),
|
|
116
|
+
{ method: "HEAD" },
|
|
117
|
+
METADATA_TIMEOUT_MS
|
|
118
|
+
);
|
|
119
|
+
if (response.status === 404) return null;
|
|
120
|
+
if (!response.ok) return fail("HEAD", key, response);
|
|
121
|
+
return { size: Number(response.headers.get("content-length") ?? 0) };
|
|
122
|
+
},
|
|
123
|
+
async list(prefix, listOptions) {
|
|
124
|
+
const limit = listOptions?.limit;
|
|
125
|
+
const objects = [];
|
|
126
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
127
|
+
let token;
|
|
128
|
+
do {
|
|
129
|
+
const params = new URLSearchParams({ "list-type": "2", prefix });
|
|
130
|
+
if (listOptions?.delimiter)
|
|
131
|
+
params.set("delimiter", listOptions.delimiter);
|
|
132
|
+
if (limit !== void 0)
|
|
133
|
+
params.set("max-keys", String(Math.min(limit, 1e3)));
|
|
134
|
+
if (token) params.set("continuation-token", token);
|
|
135
|
+
const response = await send(
|
|
136
|
+
`${bucketUrl}?${params}`,
|
|
137
|
+
{ method: "GET" },
|
|
138
|
+
METADATA_TIMEOUT_MS
|
|
139
|
+
);
|
|
140
|
+
if (!response.ok) return fail("LIST", prefix, response);
|
|
141
|
+
const page = parseListObjectsV2(await response.text());
|
|
142
|
+
objects.push(...page.objects);
|
|
143
|
+
for (const p of page.prefixes) prefixes.add(p);
|
|
144
|
+
if (limit !== void 0 && objects.length + prefixes.size >= limit)
|
|
145
|
+
break;
|
|
146
|
+
token = page.nextToken;
|
|
147
|
+
} while (token);
|
|
148
|
+
return { objects, prefixes: [...prefixes] };
|
|
149
|
+
},
|
|
150
|
+
async copy(fromKey, toKey) {
|
|
151
|
+
const response = await send(
|
|
152
|
+
objectUrl(toKey),
|
|
153
|
+
{
|
|
154
|
+
method: "PUT",
|
|
155
|
+
headers: {
|
|
156
|
+
"x-amz-copy-source": `/${encodeURIComponent(options.bucket)}/${encodeKey(fromKey)}`
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
BODY_TIMEOUT_MS
|
|
160
|
+
);
|
|
161
|
+
if (!response.ok) return fail("COPY", toKey, response);
|
|
162
|
+
const body = await response.text();
|
|
163
|
+
if (body.includes("<Error>")) {
|
|
164
|
+
throw new S3HttpError("COPY", toKey, 500, body.slice(0, 300));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export {
|
|
170
|
+
S3HttpError,
|
|
171
|
+
createFetchS3Store,
|
|
172
|
+
parseListObjectsV2
|
|
173
|
+
};
|
|
174
|
+
//# sourceMappingURL=fetch-s3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stores/fetch-s3.ts"],"sourcesContent":["import { AwsClient } from \"aws4fetch\";\nimport type {\n\tListOptions,\n\tListResult,\n\tObjectStat,\n\tObjectStore,\n} from \"../types.js\";\n\n/**\n * An S3-compatible store (AWS S3, Cloudflare R2, MinIO, …) over the REST API,\n * signed with `aws4fetch` (optional peer dependency) and sent with plain\n * `fetch` — no AWS SDK, so it runs on any runtime with Web APIs (Node, Vercel\n * Functions, Workers). Node's built-in fetch pools keep-alive connections.\n *\n * Retries and the circuit breaker are not here: wrap the store in\n * `createRetryStore`. Every failed request throws an {@link S3HttpError}\n * carrying the status in the `$metadata.httpStatusCode` shape that\n * `isRetryableError` classifies (5xx/429 retry); a stalled request aborts with\n * a `TimeoutError`, which is retried too.\n */\n\n/** A non-2xx response (404s never throw — they map to `null`). */\nexport class S3HttpError extends Error {\n\treadonly $metadata: { httpStatusCode: number };\n\n\tconstructor(operation: string, key: string, status: number, detail: string) {\n\t\tsuper(\n\t\t\t`S3 ${operation} ${key} failed: HTTP ${status}${detail ? ` — ${detail}` : \"\"}`,\n\t\t);\n\t\tthis.name = status === 429 ? \"SlowDown\" : \"S3HttpError\";\n\t\tthis.$metadata = { httpStatusCode: status };\n\t}\n}\n\nexport interface FetchS3ObjectStore extends ObjectStore {\n\t/** Server-side copy within the bucket: no bytes pass through this process. */\n\tcopy(fromKey: string, toKey: string): Promise<void>;\n\t/**\n\t * Required, not optional as on {@link ObjectStore}: this store always serves\n\t * ranges (S3's `Range` header). Declaring it required is what makes a wrapper\n\t * that forgets to forward it a compile error — dropping it does not fail\n\t * loudly, it makes `getObjectSize` answer \"unknown\", so every size gate would\n\t * quietly read the whole object instead of refusing it.\n\t */\n\tgetRange(key: string, start: number, end: number): Promise<Uint8Array | null>;\n}\n\nfunction encodeKey(key: string): string {\n\treturn key.split(\"/\").map(encodeURIComponent).join(\"/\");\n}\n\nfunction decodeXml(text: string): string {\n\treturn text\n\t\t.replace(/</g, \"<\")\n\t\t.replace(/>/g, \">\")\n\t\t.replace(/"/g, '\"')\n\t\t.replace(/'/g, \"'\")\n\t\t.replace(/&#(\\d+);/g, (_, code: string) =>\n\t\t\tString.fromCodePoint(Number(code)),\n\t\t)\n\t\t.replace(/&#x([0-9a-f]+);/gi, (_, code: string) =>\n\t\t\tString.fromCodePoint(Number.parseInt(code, 16)),\n\t\t)\n\t\t.replace(/&/g, \"&\");\n}\n\nfunction xmlValues(xml: string, tag: string): string[] {\n\tconst pattern = new RegExp(`<${tag}>([\\\\s\\\\S]*?)</${tag}>`, \"g\");\n\treturn [...xml.matchAll(pattern)].map((match) => decodeXml(match[1] ?? \"\"));\n}\n\n/** Parses one ListObjectsV2 response page (exported for tests). */\nexport function parseListObjectsV2(xml: string): {\n\tobjects: ListResult[\"objects\"];\n\tprefixes: string[];\n\tnextToken: string | undefined;\n} {\n\tconst objects = xmlValues(xml, \"Contents\").map((entry) => ({\n\t\tkey: xmlValues(entry, \"Key\")[0] ?? \"\",\n\t\tsize: Number(xmlValues(entry, \"Size\")[0] ?? 0),\n\t}));\n\tconst prefixes = xmlValues(xml, \"CommonPrefixes\").flatMap((entry) =>\n\t\txmlValues(entry, \"Prefix\"),\n\t);\n\tconst truncated = xmlValues(xml, \"IsTruncated\")[0] === \"true\";\n\tconst nextToken = truncated\n\t\t? xmlValues(xml, \"NextContinuationToken\")[0]\n\t\t: undefined;\n\treturn { objects, prefixes, nextToken };\n}\n\nexport function createFetchS3Store(options: {\n\t/** Service endpoint, e.g. `https://<account>.r2.cloudflarestorage.com`. */\n\tendpoint: string;\n\tbucket: string;\n\taccessKeyId: string;\n\tsecretAccessKey: string;\n\t/** Signing region. Default `\"auto\"` (R2); use e.g. `\"us-east-1\"` for AWS. */\n\tregion?: string;\n\t/** Per-attempt timeout for body transfers (get/put/copy). Default 30 000 ms. */\n\tbodyTimeoutMs?: number;\n\t/** Per-attempt timeout for metadata calls (head/list/range). Default 10 000 ms. */\n\tmetadataTimeoutMs?: number;\n\t/** Derive a Content-Type for uploads from the key. */\n\tcontentType?: (key: string) => string | undefined;\n\t/** Custom `fetch` (tests, proxies). Default: the global one. */\n\tfetch?: typeof fetch;\n}): FetchS3ObjectStore {\n\tconst BODY_TIMEOUT_MS = options.bodyTimeoutMs ?? 30_000;\n\tconst METADATA_TIMEOUT_MS = options.metadataTimeoutMs ?? 10_000;\n\tconst client = new AwsClient({\n\t\taccessKeyId: options.accessKeyId,\n\t\tsecretAccessKey: options.secretAccessKey,\n\t\tservice: \"s3\",\n\t\tregion: options.region ?? \"auto\",\n\t\t// createRetryStore owns retries; aws4fetch's own would stack under it.\n\t\tretries: 0,\n\t});\n\tconst doFetch = options.fetch ?? fetch;\n\tconst bucketUrl = `${options.endpoint.replace(/\\/+$/, \"\")}/${encodeURIComponent(options.bucket)}`;\n\n\tasync function send(\n\t\turl: string,\n\t\tinit: RequestInit,\n\t\ttimeoutMs: number,\n\t): Promise<Response> {\n\t\tconst request = await client.sign(url, init);\n\t\treturn doFetch(request, { signal: AbortSignal.timeout(timeoutMs) });\n\t}\n\n\tasync function fail(\n\t\toperation: string,\n\t\tkey: string,\n\t\tresponse: Response,\n\t): Promise<never> {\n\t\tconst detail = (await response.text().catch(() => \"\")).slice(0, 300);\n\t\tthrow new S3HttpError(operation, key, response.status, detail);\n\t}\n\n\tconst objectUrl = (key: string) => `${bucketUrl}/${encodeKey(key)}`;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst response = await send(\n\t\t\t\tobjectUrl(key),\n\t\t\t\t{ method: \"GET\" },\n\t\t\t\tBODY_TIMEOUT_MS,\n\t\t\t);\n\t\t\tif (response.status === 404) {\n\t\t\t\tawait response.body?.cancel();\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (!response.ok) return fail(\"GET\", key, response);\n\t\t\treturn new Uint8Array(await response.arrayBuffer());\n\t\t},\n\n\t\tasync getRange(key, start, end) {\n\t\t\tconst response = await send(\n\t\t\t\tobjectUrl(key),\n\t\t\t\t{ method: \"GET\", headers: { Range: `bytes=${start}-${end}` } },\n\t\t\t\tMETADATA_TIMEOUT_MS,\n\t\t\t);\n\t\t\tif (response.status === 404) {\n\t\t\t\tawait response.body?.cancel();\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (!response.ok) return fail(\"GET range\", key, response);\n\t\t\treturn new Uint8Array(await response.arrayBuffer());\n\t\t},\n\n\t\tasync put(key, data) {\n\t\t\tconst headers: Record<string, string> = {};\n\t\t\tconst type = options.contentType?.(key);\n\t\t\tif (type) headers[\"Content-Type\"] = type;\n\t\t\tconst response = await send(\n\t\t\t\tobjectUrl(key),\n\t\t\t\t{ method: \"PUT\", headers, body: data as Uint8Array<ArrayBuffer> },\n\t\t\t\tBODY_TIMEOUT_MS,\n\t\t\t);\n\t\t\tif (!response.ok) return fail(\"PUT\", key, response);\n\t\t\tawait response.body?.cancel();\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tconst response = await send(\n\t\t\t\tobjectUrl(key),\n\t\t\t\t{ method: \"DELETE\" },\n\t\t\t\tMETADATA_TIMEOUT_MS,\n\t\t\t);\n\t\t\t// S3 answers 204 for a missing key; tolerate 404 from compatibles.\n\t\t\tif (!response.ok && response.status !== 404) {\n\t\t\t\treturn fail(\"DELETE\", key, response);\n\t\t\t}\n\t\t\tawait response.body?.cancel();\n\t\t},\n\n\t\tasync head(key): Promise<ObjectStat | null> {\n\t\t\tconst response = await send(\n\t\t\t\tobjectUrl(key),\n\t\t\t\t{ method: \"HEAD\" },\n\t\t\t\tMETADATA_TIMEOUT_MS,\n\t\t\t);\n\t\t\tif (response.status === 404) return null;\n\t\t\tif (!response.ok) return fail(\"HEAD\", key, response);\n\t\t\treturn { size: Number(response.headers.get(\"content-length\") ?? 0) };\n\t\t},\n\n\t\tasync list(prefix, listOptions?: ListOptions): Promise<ListResult> {\n\t\t\tconst limit = listOptions?.limit;\n\t\t\tconst objects: ListResult[\"objects\"] = [];\n\t\t\tconst prefixes = new Set<string>();\n\t\t\tlet token: string | undefined;\n\n\t\t\tdo {\n\t\t\t\tconst params = new URLSearchParams({ \"list-type\": \"2\", prefix });\n\t\t\t\tif (listOptions?.delimiter)\n\t\t\t\t\tparams.set(\"delimiter\", listOptions.delimiter);\n\t\t\t\tif (limit !== undefined)\n\t\t\t\t\tparams.set(\"max-keys\", String(Math.min(limit, 1000)));\n\t\t\t\tif (token) params.set(\"continuation-token\", token);\n\t\t\t\tconst response = await send(\n\t\t\t\t\t`${bucketUrl}?${params}`,\n\t\t\t\t\t{ method: \"GET\" },\n\t\t\t\t\tMETADATA_TIMEOUT_MS,\n\t\t\t\t);\n\t\t\t\tif (!response.ok) return fail(\"LIST\", prefix, response);\n\t\t\t\tconst page = parseListObjectsV2(await response.text());\n\t\t\t\tobjects.push(...page.objects);\n\t\t\t\tfor (const p of page.prefixes) prefixes.add(p);\n\t\t\t\tif (limit !== undefined && objects.length + prefixes.size >= limit)\n\t\t\t\t\tbreak;\n\t\t\t\ttoken = page.nextToken;\n\t\t\t} while (token);\n\n\t\t\treturn { objects, prefixes: [...prefixes] };\n\t\t},\n\n\t\tasync copy(fromKey, toKey) {\n\t\t\tconst response = await send(\n\t\t\t\tobjectUrl(toKey),\n\t\t\t\t{\n\t\t\t\t\tmethod: \"PUT\",\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t\"x-amz-copy-source\": `/${encodeURIComponent(options.bucket)}/${encodeKey(fromKey)}`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tBODY_TIMEOUT_MS,\n\t\t\t);\n\t\t\tif (!response.ok) return fail(\"COPY\", toKey, response);\n\t\t\t// A copy can answer 200 with an <Error> body (S3 semantics).\n\t\t\tconst body = await response.text();\n\t\t\tif (body.includes(\"<Error>\")) {\n\t\t\t\tthrow new S3HttpError(\"COPY\", toKey, 500, body.slice(0, 300));\n\t\t\t}\n\t\t},\n\t};\n}\n"],"mappings":"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","names":[]}
|
package/dist/http.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
packBaseName,
|
|
8
8
|
resolveLooseRefFast,
|
|
9
9
|
storePack
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-IAKZRWBZ.js";
|
|
11
11
|
import {
|
|
12
12
|
OBJ_OFS_DELTA,
|
|
13
13
|
OBJ_REF_DELTA,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
runStep,
|
|
29
29
|
sha1,
|
|
30
30
|
toHex
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-OKBQISUC.js";
|
|
32
32
|
|
|
33
33
|
// src/http/info-refs.ts
|
|
34
34
|
import git from "isomorphic-git";
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MemoryObjectStore
|
|
3
|
+
} from "./chunk-HLYCSNJ2.js";
|
|
1
4
|
import {
|
|
2
5
|
classifyGitdirPath,
|
|
3
6
|
getObjectSize,
|
|
4
7
|
isMutableGitdirEntry,
|
|
5
8
|
parseLooseObjectPath
|
|
6
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-T5M5EYCZ.js";
|
|
7
10
|
import {
|
|
8
11
|
GitAuthenticationError,
|
|
9
12
|
GitAuthorizationError,
|
|
@@ -27,6 +30,7 @@ import {
|
|
|
27
30
|
fromHex,
|
|
28
31
|
hasNullByte,
|
|
29
32
|
isFullSha,
|
|
33
|
+
isNotFoundError,
|
|
30
34
|
isSafeBranchName,
|
|
31
35
|
isSafeFullRefName,
|
|
32
36
|
isSafeRefName,
|
|
@@ -40,7 +44,7 @@ import {
|
|
|
40
44
|
sha1,
|
|
41
45
|
toBase64,
|
|
42
46
|
toHex
|
|
43
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-OKBQISUC.js";
|
|
44
48
|
|
|
45
49
|
// src/cache.ts
|
|
46
50
|
import { LRUCache } from "lru-cache";
|
|
@@ -497,6 +501,35 @@ function createGitFs(store, options = {}) {
|
|
|
497
501
|
};
|
|
498
502
|
}
|
|
499
503
|
|
|
504
|
+
// src/prefix.ts
|
|
505
|
+
var BATCH = 20;
|
|
506
|
+
async function inBatches(items, fn) {
|
|
507
|
+
for (let i = 0; i < items.length; i += BATCH) {
|
|
508
|
+
await Promise.all(items.slice(i, i + BATCH).map(fn));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function deletePrefix(store, prefix) {
|
|
512
|
+
const { objects } = await store.list(prefix);
|
|
513
|
+
await inBatches(objects, (object) => store.delete(object.key));
|
|
514
|
+
return objects.length;
|
|
515
|
+
}
|
|
516
|
+
async function movePrefix(store, from, to, options = {}) {
|
|
517
|
+
if (from.startsWith(to) || to.startsWith(from)) {
|
|
518
|
+
throw new Error(`movePrefix: prefixes overlap (${from} -> ${to})`);
|
|
519
|
+
}
|
|
520
|
+
const copy = options.copy ?? (async (fromKey, toKey2) => {
|
|
521
|
+
const data = await store.get(fromKey);
|
|
522
|
+
if (data) await store.put(toKey2, data);
|
|
523
|
+
});
|
|
524
|
+
const { objects } = await store.list(from);
|
|
525
|
+
await inBatches(
|
|
526
|
+
objects,
|
|
527
|
+
(object) => copy(object.key, to + object.key.slice(from.length))
|
|
528
|
+
);
|
|
529
|
+
await inBatches(objects, (object) => store.delete(object.key));
|
|
530
|
+
return objects.length;
|
|
531
|
+
}
|
|
532
|
+
|
|
500
533
|
// src/retry.ts
|
|
501
534
|
var CircuitOpenError = class extends Error {
|
|
502
535
|
code = "EUNAVAILABLE";
|
|
@@ -522,7 +555,8 @@ var RETRYABLE_CODES = /* @__PURE__ */ new Set([
|
|
|
522
555
|
"EAI_AGAIN",
|
|
523
556
|
"EPROTO"
|
|
524
557
|
]);
|
|
525
|
-
function
|
|
558
|
+
function isRetryableError(error) {
|
|
559
|
+
if (error instanceof TypeError) return true;
|
|
526
560
|
if (typeof error !== "object" || error === null) return false;
|
|
527
561
|
const err = error;
|
|
528
562
|
if (err.name !== void 0 && RETRYABLE_NAMES.has(err.name)) return true;
|
|
@@ -536,7 +570,7 @@ function createRetryStore(store, options = {}) {
|
|
|
536
570
|
const initialDelayMs = options.initialDelayMs ?? 100;
|
|
537
571
|
const maxDelayMs = options.maxDelayMs ?? 5e3;
|
|
538
572
|
const jitter = options.jitter ?? 0.3;
|
|
539
|
-
const isRetryable = options.isRetryable ??
|
|
573
|
+
const isRetryable = options.isRetryable ?? isRetryableError;
|
|
540
574
|
const breaker = options.breaker === false ? null : {
|
|
541
575
|
threshold: options.breaker?.threshold ?? 5,
|
|
542
576
|
resetMs: options.breaker?.resetMs ?? 3e4
|
|
@@ -604,55 +638,6 @@ function createRetryStore(store, options = {}) {
|
|
|
604
638
|
} : {}
|
|
605
639
|
};
|
|
606
640
|
}
|
|
607
|
-
|
|
608
|
-
// src/stores/memory.ts
|
|
609
|
-
var MemoryObjectStore = class {
|
|
610
|
-
objects = /* @__PURE__ */ new Map();
|
|
611
|
-
async get(key) {
|
|
612
|
-
const data = this.objects.get(key);
|
|
613
|
-
return data ? data.slice() : null;
|
|
614
|
-
}
|
|
615
|
-
async getRange(key, start, end) {
|
|
616
|
-
const data = this.objects.get(key);
|
|
617
|
-
return data ? data.slice(start, end + 1) : null;
|
|
618
|
-
}
|
|
619
|
-
async put(key, data) {
|
|
620
|
-
this.objects.set(key, data.slice());
|
|
621
|
-
}
|
|
622
|
-
async delete(key) {
|
|
623
|
-
this.objects.delete(key);
|
|
624
|
-
}
|
|
625
|
-
async head(key) {
|
|
626
|
-
const data = this.objects.get(key);
|
|
627
|
-
return data ? { size: data.byteLength } : null;
|
|
628
|
-
}
|
|
629
|
-
async list(prefix, options) {
|
|
630
|
-
const delimiter = options?.delimiter;
|
|
631
|
-
const limit = options?.limit ?? Number.POSITIVE_INFINITY;
|
|
632
|
-
const objects = [];
|
|
633
|
-
const prefixes = /* @__PURE__ */ new Set();
|
|
634
|
-
for (const [key, data] of this.objects) {
|
|
635
|
-
if (!key.startsWith(prefix)) continue;
|
|
636
|
-
const rest = key.slice(prefix.length);
|
|
637
|
-
if (delimiter !== void 0) {
|
|
638
|
-
const idx = rest.indexOf(delimiter);
|
|
639
|
-
if (idx !== -1) {
|
|
640
|
-
prefixes.add(prefix + rest.slice(0, idx + delimiter.length));
|
|
641
|
-
} else {
|
|
642
|
-
objects.push({ key, size: data.byteLength });
|
|
643
|
-
}
|
|
644
|
-
} else {
|
|
645
|
-
objects.push({ key, size: data.byteLength });
|
|
646
|
-
}
|
|
647
|
-
if (objects.length + prefixes.size >= limit) break;
|
|
648
|
-
}
|
|
649
|
-
return { objects, prefixes: [...prefixes] };
|
|
650
|
-
}
|
|
651
|
-
/** Number of stored objects (test convenience, not part of ObjectStore). */
|
|
652
|
-
get size() {
|
|
653
|
-
return this.objects.size;
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
641
|
export {
|
|
657
642
|
CircuitOpenError,
|
|
658
643
|
FsError,
|
|
@@ -678,6 +663,7 @@ export {
|
|
|
678
663
|
decodeAscii,
|
|
679
664
|
decodeUtf8,
|
|
680
665
|
deflate,
|
|
666
|
+
deletePrefix,
|
|
681
667
|
encodeUtf8,
|
|
682
668
|
formatErrorResponse,
|
|
683
669
|
fromHex,
|
|
@@ -685,11 +671,14 @@ export {
|
|
|
685
671
|
hasNullByte,
|
|
686
672
|
isFullSha,
|
|
687
673
|
isMutableGitdirEntry,
|
|
674
|
+
isNotFoundError,
|
|
675
|
+
isRetryableError,
|
|
688
676
|
isSafeBranchName,
|
|
689
677
|
isSafeFullRefName,
|
|
690
678
|
isSafeRefName,
|
|
691
679
|
isSafeRepoPath,
|
|
692
680
|
isSafeTreePath,
|
|
681
|
+
movePrefix,
|
|
693
682
|
packFileName,
|
|
694
683
|
parseLooseObjectPath,
|
|
695
684
|
parsePackDirectory,
|