git-fs-s3 0.15.0 → 0.15.3

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/index.js CHANGED
@@ -1,692 +1,612 @@
1
- import {
2
- MemoryObjectStore
3
- } from "./chunk-HLYCSNJ2.js";
4
- import {
5
- classifyGitdirPath,
6
- getObjectSize,
7
- isMutableGitdirEntry,
8
- parseLooseObjectPath
9
- } from "./chunk-T5M5EYCZ.js";
10
- import {
11
- GitAuthenticationError,
12
- GitAuthorizationError,
13
- GitConflictError,
14
- GitError,
15
- GitInvalidRequestError,
16
- GitObjectNotFoundError,
17
- GitPathNotFoundError,
18
- GitProtocolError,
19
- GitRateLimitError,
20
- GitRefNotFoundError,
21
- GitRepositoryNotFoundError,
22
- PACK_FILE_KINDS,
23
- READABLE_PACK_FILE_KINDS,
24
- concat,
25
- decodeAscii,
26
- decodeUtf8,
27
- deflate,
28
- encodeUtf8,
29
- formatErrorResponse,
30
- fromHex,
31
- hasNullByte,
32
- isFullSha,
33
- isNotFoundError,
34
- isSafeBranchName,
35
- isSafeFullRefName,
36
- isSafeRefName,
37
- isSafeRepoPath,
38
- isSafeTreePath,
39
- packFileName,
40
- parsePackDirectory,
41
- parsePackFileName,
42
- qualifyBranchRef,
43
- readBlobContent,
44
- sha1,
45
- toBase64,
46
- toHex
47
- } from "./chunk-OKBQISUC.js";
48
-
49
- // src/cache.ts
1
+ import { A as GitRefNotFoundError, B as fromHex, C as GitConflictError, D as GitPathNotFoundError, E as GitObjectNotFoundError, F as concat, G as sha1, I as decodeAscii, K as toBase64, L as decodeUtf8, M as formatErrorResponse, N as isAbsentError, O as GitProtocolError, P as isNotFoundError, R as deflate, S as GitAuthorizationError, T as GitInvalidRequestError, V as hasNullByte, W as readBlobContent, _ as READABLE_PACK_FILE_KINDS, a as isSafeRepoPath, b as parsePackFileName, g as PACK_FILE_KINDS, i as isSafeRefName, j as GitRepositoryNotFoundError, k as GitRateLimitError, n as isSafeBranchName, o as isSafeTreePath, q as toHex, r as isSafeFullRefName, s as qualifyBranchRef, t as isFullSha, v as packFileName, w as GitError, x as GitAuthenticationError, y as parsePackDirectory, z as encodeUtf8 } from "./refs-Cb_NfKWI.js";
2
+ import { i as parseLooseObjectPath, n as classifyGitdirPath, r as isMutableGitdirEntry, t as getObjectSize } from "./object-size-BJAsnZzw.js";
3
+ import { t as MemoryObjectStore } from "./memory-M2cCuVF-.js";
50
4
  import { LRUCache } from "lru-cache";
51
- var MISS = /* @__PURE__ */ Symbol("miss");
5
+ //#region src/cache.ts
6
+ const MISS = Symbol("miss");
52
7
  function listEntrySize(entry) {
53
- let size = entry.prefix.length + 16;
54
- for (const o of entry.result.objects) size += o.key.length + 8;
55
- for (const p of entry.result.prefixes) size += p.length;
56
- return size;
8
+ let size = entry.prefix.length + 16;
9
+ for (const o of entry.result.objects) size += o.key.length + 8;
10
+ for (const p of entry.result.prefixes) size += p.length;
11
+ return size;
57
12
  }
58
13
  function copyListResult(result) {
59
- return {
60
- objects: result.objects.map((o) => ({ ...o })),
61
- prefixes: [...result.prefixes]
62
- };
14
+ return {
15
+ objects: result.objects.map((o) => ({ ...o })),
16
+ prefixes: [...result.prefixes]
17
+ };
63
18
  }
19
+ /**
20
+ * Wrap an {@link ObjectStore} with an in-process LRU read cache.
21
+ *
22
+ * Git object keys are content-addressed and therefore immutable, which makes
23
+ * them ideal cache entries; mutable keys (refs, packed-refs) are bounded by
24
+ * `ttlMs`. Writes and deletes through this wrapper invalidate their key and
25
+ * any cached listings they affect — with one asymmetry: a non-empty `limit: 1`
26
+ * probe (a "directory exists" answer) survives writes underneath it, because
27
+ * adding a key below a prefix cannot make that prefix stop existing, while
28
+ * empty probes and full listings are always dropped.
29
+ */
64
30
  function createCachedStore(store, options = {}) {
65
- const maxBytes = options.maxBytes ?? 50 * 1024 * 1024;
66
- const maxEntryBytes = options.maxEntryBytes ?? Math.ceil(maxBytes / 10);
67
- const ttl = options.ttlMs ?? 6e4;
68
- const ttlForKey = options.ttlForKey;
69
- const cacheMisses = options.cacheMisses ?? false;
70
- const cacheLists = options.cacheLists ?? false;
71
- const range = store.getRange?.bind(store);
72
- const coalesce = options.coalesce ?? true;
73
- const copy = options.copy ?? true;
74
- const clone = (data) => copy ? data.slice() : data;
75
- const onHit = options.onHit;
76
- const onMiss = options.onMiss;
77
- const cache = new LRUCache({
78
- maxSize: maxBytes,
79
- sizeCalculation: (value) => value === MISS ? 1 : value.byteLength || 1,
80
- ttl
81
- });
82
- const listCache = new LRUCache({
83
- maxSize: Math.max(1, Math.ceil(maxBytes / 10)),
84
- sizeCalculation: listEntrySize,
85
- ttl
86
- });
87
- const pendingGets = /* @__PURE__ */ new Map();
88
- const pendingHeads = /* @__PURE__ */ new Map();
89
- const pendingLists = /* @__PURE__ */ new Map();
90
- let generation = 0;
91
- const unchangedSince = (start) => generation === start;
92
- const admit = (key, data) => {
93
- if (data.byteLength <= maxEntryBytes) {
94
- cache.set(key, clone(data), { ttl: ttlForKey?.(key) });
95
- }
96
- };
97
- function clearStaleListEntries(key) {
98
- for (const [listKey, entry] of listCache.entries()) {
99
- if (!key.startsWith(entry.prefix)) continue;
100
- if (entry.probe && !entry.empty) continue;
101
- listCache.delete(listKey);
102
- }
103
- for (const listKey of pendingLists.keys()) {
104
- const prefix = listKey.split("|").slice(2).join("|");
105
- if (key.startsWith(prefix)) pendingLists.delete(listKey);
106
- }
107
- }
108
- function noteWrite(key) {
109
- generation++;
110
- pendingGets.delete(key);
111
- pendingHeads.delete(key);
112
- }
113
- function coalesced(pending, key, fn) {
114
- if (!coalesce) return fn();
115
- const inflight = pending.get(key);
116
- if (inflight !== void 0) return inflight;
117
- const p = fn().finally(() => {
118
- if (pending.get(key) === p) pending.delete(key);
119
- });
120
- pending.set(key, p);
121
- return p;
122
- }
123
- return {
124
- async get(key) {
125
- const cached = cache.get(key);
126
- if (cached !== void 0) {
127
- onHit?.(key);
128
- return cached === MISS ? null : clone(cached);
129
- }
130
- const data = await coalesced(pendingGets, key, async () => {
131
- onMiss?.(key);
132
- const start = generation;
133
- const fetched = await store.get(key);
134
- if (unchangedSince(start)) {
135
- if (fetched !== null) {
136
- admit(key, fetched);
137
- } else if (cacheMisses) {
138
- cache.set(key, MISS, { ttl: ttlForKey?.(key) });
139
- }
140
- }
141
- return fetched;
142
- });
143
- return data === null ? null : clone(data);
144
- },
145
- async put(key, data) {
146
- noteWrite(key);
147
- try {
148
- await store.put(key, data);
149
- } finally {
150
- noteWrite(key);
151
- cache.delete(key);
152
- clearStaleListEntries(key);
153
- }
154
- admit(key, data);
155
- },
156
- async delete(key) {
157
- noteWrite(key);
158
- try {
159
- await store.delete(key);
160
- } finally {
161
- noteWrite(key);
162
- cache.delete(key);
163
- clearStaleListEntries(key);
164
- }
165
- if (cacheMisses) {
166
- cache.set(key, MISS, { ttl: ttlForKey?.(key) });
167
- } else {
168
- cache.delete(key);
169
- }
170
- },
171
- async head(key) {
172
- const cached = cache.get(key);
173
- if (cached !== void 0) {
174
- onHit?.(key);
175
- return cached === MISS ? null : { size: cached.byteLength };
176
- }
177
- return coalesced(pendingHeads, key, async () => {
178
- onMiss?.(key);
179
- const start = generation;
180
- const stat = await store.head(key);
181
- if (stat === null && cacheMisses && unchangedSince(start)) {
182
- cache.set(key, MISS, { ttl: ttlForKey?.(key) });
183
- }
184
- return stat;
185
- });
186
- },
187
- async list(prefix, listOptions) {
188
- if (!cacheLists) return store.list(prefix, listOptions);
189
- const listKey = `${listOptions?.delimiter ?? ""}|${listOptions?.limit ?? ""}|${prefix}`;
190
- const cached = listCache.get(listKey);
191
- if (cached !== void 0) {
192
- onHit?.(prefix);
193
- return copyListResult(cached.result);
194
- }
195
- const result = await coalesced(pendingLists, listKey, async () => {
196
- onMiss?.(prefix);
197
- const start = generation;
198
- const fetched = await store.list(prefix, listOptions);
199
- if (!unchangedSince(start)) return fetched;
200
- listCache.set(
201
- listKey,
202
- {
203
- result: copyListResult(fetched),
204
- prefix,
205
- probe: listOptions?.limit === 1,
206
- empty: fetched.objects.length === 0 && fetched.prefixes.length === 0
207
- },
208
- { ttl: ttlForKey?.(prefix) }
209
- );
210
- return fetched;
211
- });
212
- return copyListResult(result);
213
- },
214
- // Forwarded straight to the backing store: a range read is a handful of
215
- // bytes at a known offset, so caching one buys nothing, and bypassing the
216
- // cache here keeps this wrapper's read paths unchanged.
217
- ...range ? {
218
- getRange: (key, start, end) => range(key, start, end)
219
- } : {},
220
- invalidate(prefix) {
221
- generation++;
222
- for (const key of [...pendingGets.keys()]) {
223
- if (key.startsWith(prefix)) pendingGets.delete(key);
224
- }
225
- for (const key of [...pendingHeads.keys()]) {
226
- if (key.startsWith(prefix)) pendingHeads.delete(key);
227
- }
228
- pendingLists.clear();
229
- for (const key of cache.keys()) {
230
- if (key.startsWith(prefix)) cache.delete(key);
231
- }
232
- for (const [listKey, entry] of listCache.entries()) {
233
- if (entry.prefix.startsWith(prefix) || prefix.startsWith(entry.prefix)) {
234
- listCache.delete(listKey);
235
- }
236
- }
237
- }
238
- };
31
+ const maxBytes = options.maxBytes ?? 52428800;
32
+ const maxEntryBytes = options.maxEntryBytes ?? Math.ceil(maxBytes / 10);
33
+ const ttl = options.ttlMs ?? 6e4;
34
+ const ttlForKey = options.ttlForKey;
35
+ const cacheMisses = options.cacheMisses ?? false;
36
+ const cacheLists = options.cacheLists ?? false;
37
+ const range = store.getRange?.bind(store);
38
+ const coalesce = options.coalesce ?? true;
39
+ const copy = options.copy ?? true;
40
+ const clone = (data) => copy ? data.slice() : data;
41
+ const onHit = options.onHit;
42
+ const onMiss = options.onMiss;
43
+ const cache = new LRUCache({
44
+ maxSize: maxBytes,
45
+ sizeCalculation: (value) => value === MISS ? 1 : value.byteLength || 1,
46
+ ttl
47
+ });
48
+ const listCache = new LRUCache({
49
+ maxSize: Math.max(1, Math.ceil(maxBytes / 10)),
50
+ sizeCalculation: listEntrySize,
51
+ ttl
52
+ });
53
+ const pendingGets = /* @__PURE__ */ new Map();
54
+ const pendingHeads = /* @__PURE__ */ new Map();
55
+ const pendingLists = /* @__PURE__ */ new Map();
56
+ /**
57
+ * Bumped by every write, delete and invalidation. A backend read that
58
+ * started before one of those may finish after it carrying the old answer
59
+ * (the old bytes, or "missing" for a key that now exists); it still
60
+ * answers its own callers, but only a read that saw no write in between
61
+ * may populate the cache — otherwise a stale miss could hide a freshly
62
+ * written object for the whole TTL.
63
+ */
64
+ let generation = 0;
65
+ const unchangedSince = (start) => generation === start;
66
+ const admit = (key, data) => {
67
+ if (data.byteLength <= maxEntryBytes) cache.set(key, clone(data), { ttl: ttlForKey?.(key) });
68
+ };
69
+ /** Drop list entries a write/delete at `key` may have made stale. */
70
+ function clearStaleListEntries(key) {
71
+ for (const [listKey, entry] of listCache.entries()) {
72
+ if (!key.startsWith(entry.prefix)) continue;
73
+ if (entry.probe && !entry.empty) continue;
74
+ listCache.delete(listKey);
75
+ }
76
+ for (const listKey of pendingLists.keys()) {
77
+ const prefix = listKey.split("|").slice(2).join("|");
78
+ if (key.startsWith(prefix)) pendingLists.delete(listKey);
79
+ }
80
+ }
81
+ /** A write to `key` happened: no in-flight read of it may be joined or cached. */
82
+ function noteWrite(key) {
83
+ generation++;
84
+ pendingGets.delete(key);
85
+ pendingHeads.delete(key);
86
+ }
87
+ function coalesced(pending, key, fn) {
88
+ if (!coalesce) return fn();
89
+ const inflight = pending.get(key);
90
+ if (inflight !== void 0) return inflight;
91
+ const p = fn().finally(() => {
92
+ if (pending.get(key) === p) pending.delete(key);
93
+ });
94
+ pending.set(key, p);
95
+ return p;
96
+ }
97
+ return {
98
+ async get(key) {
99
+ const cached = cache.get(key);
100
+ if (cached !== void 0) {
101
+ onHit?.(key);
102
+ return cached === MISS ? null : clone(cached);
103
+ }
104
+ const data = await coalesced(pendingGets, key, async () => {
105
+ onMiss?.(key);
106
+ const start = generation;
107
+ const fetched = await store.get(key);
108
+ if (unchangedSince(start)) {
109
+ if (fetched !== null) admit(key, fetched);
110
+ else if (cacheMisses) cache.set(key, MISS, { ttl: ttlForKey?.(key) });
111
+ }
112
+ return fetched;
113
+ });
114
+ return data === null ? null : clone(data);
115
+ },
116
+ async put(key, data) {
117
+ noteWrite(key);
118
+ try {
119
+ await store.put(key, data);
120
+ } finally {
121
+ noteWrite(key);
122
+ cache.delete(key);
123
+ clearStaleListEntries(key);
124
+ }
125
+ admit(key, data);
126
+ },
127
+ async delete(key) {
128
+ noteWrite(key);
129
+ try {
130
+ await store.delete(key);
131
+ } finally {
132
+ noteWrite(key);
133
+ cache.delete(key);
134
+ clearStaleListEntries(key);
135
+ }
136
+ if (cacheMisses) cache.set(key, MISS, { ttl: ttlForKey?.(key) });
137
+ else cache.delete(key);
138
+ },
139
+ async head(key) {
140
+ const cached = cache.get(key);
141
+ if (cached !== void 0) {
142
+ onHit?.(key);
143
+ return cached === MISS ? null : { size: cached.byteLength };
144
+ }
145
+ return coalesced(pendingHeads, key, async () => {
146
+ onMiss?.(key);
147
+ const start = generation;
148
+ const stat = await store.head(key);
149
+ if (stat === null && cacheMisses && unchangedSince(start)) cache.set(key, MISS, { ttl: ttlForKey?.(key) });
150
+ return stat;
151
+ });
152
+ },
153
+ async list(prefix, listOptions) {
154
+ if (!cacheLists) return store.list(prefix, listOptions);
155
+ const listKey = `${listOptions?.delimiter ?? ""}|${listOptions?.limit ?? ""}|${prefix}`;
156
+ const cached = listCache.get(listKey);
157
+ if (cached !== void 0) {
158
+ onHit?.(prefix);
159
+ return copyListResult(cached.result);
160
+ }
161
+ return copyListResult(await coalesced(pendingLists, listKey, async () => {
162
+ onMiss?.(prefix);
163
+ const start = generation;
164
+ const fetched = await store.list(prefix, listOptions);
165
+ if (!unchangedSince(start)) return fetched;
166
+ listCache.set(listKey, {
167
+ result: copyListResult(fetched),
168
+ prefix,
169
+ probe: listOptions?.limit === 1,
170
+ empty: fetched.objects.length === 0 && fetched.prefixes.length === 0
171
+ }, { ttl: ttlForKey?.(prefix) });
172
+ return fetched;
173
+ }));
174
+ },
175
+ ...range ? { getRange: (key, start, end) => range(key, start, end) } : {},
176
+ invalidate(prefix) {
177
+ generation++;
178
+ for (const key of [...pendingGets.keys()]) if (key.startsWith(prefix)) pendingGets.delete(key);
179
+ for (const key of [...pendingHeads.keys()]) if (key.startsWith(prefix)) pendingHeads.delete(key);
180
+ pendingLists.clear();
181
+ for (const key of cache.keys()) if (key.startsWith(prefix)) cache.delete(key);
182
+ for (const [listKey, entry] of listCache.entries()) if (entry.prefix.startsWith(prefix) || prefix.startsWith(entry.prefix)) listCache.delete(listKey);
183
+ }
184
+ };
239
185
  }
240
-
241
- // src/errors.ts
186
+ //#endregion
187
+ //#region src/errors.ts
188
+ /**
189
+ * Node-style filesystem error carrying a `code` property, which is what
190
+ * isomorphic-git inspects to distinguish "file not found" from real failures.
191
+ */
242
192
  var FsError = class extends Error {
243
- code;
244
- syscall;
245
- path;
246
- constructor(code, syscall, path) {
247
- super(`${code}: ${syscall} '${path}'`);
248
- this.name = "FsError";
249
- this.code = code;
250
- this.syscall = syscall;
251
- this.path = path;
252
- }
193
+ code;
194
+ syscall;
195
+ path;
196
+ constructor(code, syscall, path) {
197
+ super(`${code}: ${syscall} '${path}'`);
198
+ this.name = "FsError";
199
+ this.code = code;
200
+ this.syscall = syscall;
201
+ this.path = path;
202
+ }
253
203
  };
254
- var enoent = (syscall, path) => new FsError("ENOENT", syscall, path);
255
- var enotdir = (syscall, path) => new FsError("ENOTDIR", syscall, path);
256
- var enotempty = (syscall, path) => new FsError("ENOTEMPTY", syscall, path);
257
- var einval = (syscall, path) => new FsError("EINVAL", syscall, path);
258
- var eperm = (syscall, path) => new FsError("EPERM", syscall, path);
259
-
260
- // src/git-fs.ts
261
- import { LRUCache as LRUCache2 } from "lru-cache";
262
-
263
- // src/path.ts
204
+ const enoent = (syscall, path) => new FsError("ENOENT", syscall, path);
205
+ const enotdir = (syscall, path) => new FsError("ENOTDIR", syscall, path);
206
+ const enotempty = (syscall, path) => new FsError("ENOTEMPTY", syscall, path);
207
+ const einval = (syscall, path) => new FsError("EINVAL", syscall, path);
208
+ const eperm = (syscall, path) => new FsError("EPERM", syscall, path);
209
+ //#endregion
210
+ //#region src/path.ts
211
+ /**
212
+ * Normalize an absolute-or-relative filesystem path into a storage key
213
+ * segment: no leading/trailing slashes, `.` segments dropped, `..` resolved.
214
+ * A `..` that would escape the root throws EINVAL — paths handed to the fs
215
+ * must never address keys outside the configured prefix.
216
+ */
264
217
  function normalizePath(filepath) {
265
- const segments = filepath.split("/");
266
- const out = [];
267
- for (const segment of segments) {
268
- if (segment === "" || segment === ".") continue;
269
- if (segment === "..") {
270
- if (out.length === 0) throw einval("resolve", filepath);
271
- out.pop();
272
- continue;
273
- }
274
- out.push(segment);
275
- }
276
- return out.join("/");
218
+ const segments = filepath.split("/");
219
+ const out = [];
220
+ for (const segment of segments) {
221
+ if (segment === "" || segment === ".") continue;
222
+ if (segment === "..") {
223
+ if (out.length === 0) throw einval("resolve", filepath);
224
+ out.pop();
225
+ continue;
226
+ }
227
+ out.push(segment);
228
+ }
229
+ return out.join("/");
277
230
  }
231
+ /** Join a configured key prefix with a normalized path. */
278
232
  function toKey(prefix, filepath) {
279
- const normalized = normalizePath(filepath);
280
- if (prefix === "") return normalized;
281
- return normalized === "" ? prefix : `${prefix}/${normalized}`;
233
+ const normalized = normalizePath(filepath);
234
+ if (prefix === "") return normalized;
235
+ return normalized === "" ? prefix : `${prefix}/${normalized}`;
282
236
  }
283
-
284
- // src/git-fs.ts
285
- var FILE_MODE = 33188;
286
- var DIR_MODE = 16384;
287
- var textEncoder = new TextEncoder();
288
- var textDecoder = new TextDecoder();
237
+ //#endregion
238
+ //#region src/git-fs.ts
239
+ const FILE_MODE = 33188;
240
+ const DIR_MODE = 16384;
241
+ const textEncoder = new TextEncoder();
242
+ const textDecoder = new TextDecoder();
289
243
  function makeStat(type, size) {
290
- const epoch = /* @__PURE__ */ new Date(0);
291
- return {
292
- type,
293
- mode: type === "file" ? FILE_MODE : DIR_MODE,
294
- size,
295
- ino: 0,
296
- mtimeMs: 0,
297
- ctimeMs: 0,
298
- uid: 0,
299
- gid: 0,
300
- dev: 0,
301
- mtime: epoch,
302
- ctime: epoch,
303
- isFile: () => type === "file",
304
- isDirectory: () => type === "dir",
305
- isSymbolicLink: () => false
306
- };
244
+ const epoch = /* @__PURE__ */ new Date(0);
245
+ return {
246
+ type,
247
+ mode: type === "file" ? FILE_MODE : DIR_MODE,
248
+ size,
249
+ ino: 0,
250
+ mtimeMs: 0,
251
+ ctimeMs: 0,
252
+ uid: 0,
253
+ gid: 0,
254
+ dev: 0,
255
+ mtime: epoch,
256
+ ctime: epoch,
257
+ isFile: () => type === "file",
258
+ isDirectory: () => type === "dir",
259
+ isSymbolicLink: () => false
260
+ };
307
261
  }
308
262
  function resolveEncoding(options) {
309
- if (typeof options === "string") return options;
310
- return options?.encoding;
263
+ if (typeof options === "string") return options;
264
+ return options?.encoding;
311
265
  }
266
+ /**
267
+ * Create a promise-based filesystem client for isomorphic-git backed by an
268
+ * {@link ObjectStore}.
269
+ *
270
+ * Semantics:
271
+ * - Directories are implicit, as in object storage: `mkdir` is a no-op and a
272
+ * directory "exists" whenever at least one key lives under its prefix.
273
+ * - Symbolic links are not supported (`readlink`/`symlink` throw). Bare
274
+ * repositories never contain them.
275
+ * - Designed for bare, server-side repositories (`git.init({bare: true})`,
276
+ * plumbing commands, ref updates). Worktree checkouts belong on a real disk.
277
+ */
312
278
  function createGitFs(store, options = {}) {
313
- const prefix = options.prefix ?? "";
314
- const useLooseHints = options.looseObjectHints ?? false;
315
- const onNote = options.onNote;
316
- const toKey2 = (path) => toKey(prefix, path);
317
- const looseHints = new LRUCache2({
318
- max: 1024,
319
- ttl: options.hintTtlMs ?? 36e5
320
- });
321
- const registeredGitdirs = new LRUCache2({
322
- max: 4096
323
- });
324
- function registeredAbsent(path) {
325
- const slash = path.lastIndexOf("/");
326
- if (slash < 0) return false;
327
- const absent = registeredGitdirs.get(path.slice(0, slash));
328
- return absent?.has(path.slice(slash + 1)) ?? false;
329
- }
330
- function looseScope(path) {
331
- return parseLooseObjectPath(path)?.gitdir ?? null;
332
- }
333
- const pendingDetections = /* @__PURE__ */ new Map();
334
- async function knownAbsent(path) {
335
- if (registeredAbsent(path)) return true;
336
- if (!useLooseHints) return false;
337
- const scope = looseScope(path);
338
- if (scope === null) return false;
339
- if (!looseHints.has(scope)) {
340
- let pending = pendingDetections.get(scope);
341
- if (!pending) {
342
- pending = detectLooseObjects(scope).finally(
343
- () => pendingDetections.delete(scope)
344
- );
345
- pendingDetections.set(scope, pending);
346
- }
347
- await pending;
348
- }
349
- return looseHints.get(scope) === "none";
350
- }
351
- async function isDirectory(dirKey) {
352
- const { objects, prefixes } = await store.list(`${dirKey}/`, {
353
- limit: 1
354
- });
355
- return objects.length > 0 || prefixes.length > 0;
356
- }
357
- async function stat(filepath, syscall) {
358
- const path = normalizePath(filepath);
359
- if (await knownAbsent(path)) throw enoent(syscall, filepath);
360
- const k = toKey2(path);
361
- if (k === prefix || k === "" || registeredGitdirs.has(path)) {
362
- return makeStat("dir", 0);
363
- }
364
- const fileStat = await store.head(k);
365
- if (fileStat) return makeStat("file", fileStat.size);
366
- if (useLooseHints && looseScope(path) !== null) {
367
- throw enoent(syscall, filepath);
368
- }
369
- if (await isDirectory(k)) return makeStat("dir", 0);
370
- throw enoent(syscall, filepath);
371
- }
372
- const range = store.getRange?.bind(store);
373
- const promises = {
374
- // Only defined when the backing store supports ranges, so a caller can
375
- // test for it rather than have to interpret a failure.
376
- ...range ? {
377
- getRange: (filepath, start, end) => range(toKey2(normalizePath(filepath)), start, end)
378
- } : {},
379
- async readFile(filepath, opts) {
380
- const path = normalizePath(filepath);
381
- if (await knownAbsent(path)) throw enoent("open", filepath);
382
- const data = await store.get(toKey2(path));
383
- if (data === null) throw enoent("open", filepath);
384
- return resolveEncoding(opts) === "utf8" ? textDecoder.decode(data) : data;
385
- },
386
- async writeFile(filepath, data, _opts) {
387
- const path = normalizePath(filepath);
388
- if (useLooseHints) {
389
- const scope = looseScope(path);
390
- if (scope !== null) looseHints.set(scope, "present");
391
- }
392
- const bytes = typeof data === "string" ? textEncoder.encode(data) : data;
393
- await store.put(toKey2(path), bytes);
394
- },
395
- async unlink(filepath) {
396
- const k = toKey2(normalizePath(filepath));
397
- if (await store.head(k) === null) throw enoent("unlink", filepath);
398
- await store.delete(k);
399
- },
400
- async readdir(dirpath) {
401
- const k = toKey2(normalizePath(dirpath));
402
- const isRoot = k === prefix || k === "";
403
- const listPrefix = isRoot && k === "" ? "" : `${k}/`;
404
- const { objects, prefixes } = await store.list(listPrefix, {
405
- delimiter: "/"
406
- });
407
- if (objects.length === 0 && prefixes.length === 0) {
408
- if (!isRoot && await store.head(k) !== null) {
409
- throw enotdir("scandir", dirpath);
410
- }
411
- if (!isRoot) throw enoent("scandir", dirpath);
412
- }
413
- const names = objects.map((o) => o.key.slice(listPrefix.length));
414
- const dirNames = prefixes.map(
415
- (p) => p.slice(listPrefix.length).replace(/\/$/, "")
416
- );
417
- return [...names, ...dirNames].sort();
418
- },
419
- async mkdir(_dirpath, _opts) {
420
- },
421
- async rmdir(dirpath) {
422
- const k = toKey2(normalizePath(dirpath));
423
- const { objects, prefixes } = await store.list(`${k}/`, { limit: 1 });
424
- if (objects.length > 0 || prefixes.length > 0) {
425
- throw enotempty("rmdir", dirpath);
426
- }
427
- },
428
- stat: (filepath) => stat(filepath, "stat"),
429
- lstat: (filepath) => stat(filepath, "lstat"),
430
- async readlink(filepath) {
431
- throw enoent("readlink", filepath);
432
- },
433
- async symlink(_target, filepath) {
434
- throw eperm("symlink", filepath);
435
- },
436
- async chmod(_filepath, _mode) {
437
- }
438
- };
439
- async function detectLooseObjects(gitdir) {
440
- if (!useLooseHints) return;
441
- const scope = normalizePath(gitdir);
442
- if (looseHints.has(scope)) return;
443
- try {
444
- const { objects } = await store.list(`${toKey2(scope)}/objects/`, {
445
- limit: 1
446
- });
447
- const first = objects[0]?.key;
448
- const hint = first !== void 0 && parseLooseObjectPath(first) !== null ? "present" : "none";
449
- looseHints.set(scope, hint);
450
- onNote?.(`loose objects ${hint} under ${scope}`);
451
- } catch {
452
- }
453
- }
454
- async function prefetchPacks(gitdir, prefetchOptions) {
455
- const maxPacks = prefetchOptions?.maxPacks ?? 30;
456
- const packDir = `${normalizePath(gitdir)}/objects/pack`;
457
- const entries = parsePackDirectory(
458
- await promises.readdir(packDir).catch(() => [])
459
- ).filter((file) => READABLE_PACK_FILE_KINDS.has(file.kind)).map((file) => file.name);
460
- if (entries.length > maxPacks * 2) {
461
- await detectLooseObjects(gitdir);
462
- return;
463
- }
464
- await Promise.all([
465
- detectLooseObjects(gitdir),
466
- ...entries.map(
467
- (name) => promises.readFile(`${packDir}/${name}`).catch(() => void 0)
468
- )
469
- ]);
470
- }
471
- async function listFilesRecursively(dirpath) {
472
- const directory = toKey2(normalizePath(dirpath));
473
- const listPrefix = directory === "" ? "" : `${directory}/`;
474
- const { objects } = await store.list(listPrefix);
475
- return objects.map((object) => object.key.slice(listPrefix.length)).filter((path) => path.length > 0).sort();
476
- }
477
- function registerGitdir(gitdir, facts) {
478
- registeredGitdirs.set(
479
- normalizePath(gitdir),
480
- new Set(facts?.absentFiles ?? [])
481
- );
482
- }
483
- function invalidate(pathPrefix) {
484
- const normalized = normalizePath(pathPrefix);
485
- for (const dir of registeredGitdirs.keys()) {
486
- if (dir.startsWith(normalized)) registeredGitdirs.delete(dir);
487
- }
488
- for (const scope of looseHints.keys()) {
489
- if (scope.startsWith(normalized)) looseHints.delete(scope);
490
- }
491
- const maybe = store;
492
- maybe.invalidate?.(toKey2(normalized));
493
- }
494
- return {
495
- promises,
496
- listFilesRecursively,
497
- detectLooseObjects,
498
- prefetchPacks,
499
- invalidate,
500
- registerGitdir
501
- };
279
+ const prefix = options.prefix ?? "";
280
+ const useLooseHints = options.looseObjectHints ?? false;
281
+ const onNote = options.onNote;
282
+ const toKey$1 = (path) => toKey(prefix, path);
283
+ /**
284
+ * Per-gitdir "does any loose object exist" hint. Entries are only created
285
+ * by {@link GitFs.detectLooseObjects}, so a pathological ref that merely
286
+ * looks like a loose object (`refs/heads/objects/aa/…`) derives a scope
287
+ * that was never registered and can never be wrongly short-circuited.
288
+ */
289
+ const looseHints = new LRUCache({
290
+ max: 1024,
291
+ ttl: options.hintTtlMs ?? 36e5
292
+ });
293
+ /** Gitdirs the caller vouched for via {@link GitFs.registerGitdir}. */
294
+ const registeredGitdirs = new LRUCache({ max: 4096 });
295
+ /** Whether `path` is a top-level file its registered gitdir lacks. */
296
+ function registeredAbsent(path) {
297
+ const slash = path.lastIndexOf("/");
298
+ if (slash < 0) return false;
299
+ return registeredGitdirs.get(path.slice(0, slash))?.has(path.slice(slash + 1)) ?? false;
300
+ }
301
+ /** The gitdir scope of a loose-object path, or null when it isn't one. */
302
+ function looseScope(path) {
303
+ return parseLooseObjectPath(path)?.gitdir ?? null;
304
+ }
305
+ const pendingDetections = /* @__PURE__ */ new Map();
306
+ /**
307
+ * Resolve the loose-object hint for `path`'s gitdir before answering, so a
308
+ * read that runs before any explicit `detectLooseObjects` call (e.g. the
309
+ * first commit read of a request) doesn't pay a doomed GET. One coalesced
310
+ * listing per gitdir, cached for `hintTtlMs`.
311
+ */
312
+ async function knownAbsent(path) {
313
+ if (registeredAbsent(path)) return true;
314
+ if (!useLooseHints) return false;
315
+ const scope = looseScope(path);
316
+ if (scope === null) return false;
317
+ if (!looseHints.has(scope)) {
318
+ let pending = pendingDetections.get(scope);
319
+ if (!pending) {
320
+ pending = detectLooseObjects(scope).finally(() => pendingDetections.delete(scope));
321
+ pendingDetections.set(scope, pending);
322
+ }
323
+ await pending;
324
+ }
325
+ return looseHints.get(scope) === "none";
326
+ }
327
+ async function isDirectory(dirKey) {
328
+ const { objects, prefixes } = await store.list(`${dirKey}/`, { limit: 1 });
329
+ return objects.length > 0 || prefixes.length > 0;
330
+ }
331
+ async function stat(filepath, syscall) {
332
+ const path = normalizePath(filepath);
333
+ if (await knownAbsent(path)) throw enoent(syscall, filepath);
334
+ const k = toKey$1(path);
335
+ if (k === prefix || k === "" || registeredGitdirs.has(path)) return makeStat("dir", 0);
336
+ const fileStat = await store.head(k);
337
+ if (fileStat) return makeStat("file", fileStat.size);
338
+ if (useLooseHints && looseScope(path) !== null) throw enoent(syscall, filepath);
339
+ if (await isDirectory(k)) return makeStat("dir", 0);
340
+ throw enoent(syscall, filepath);
341
+ }
342
+ const range = store.getRange?.bind(store);
343
+ const promises = {
344
+ ...range ? { getRange: (filepath, start, end) => range(toKey$1(normalizePath(filepath)), start, end) } : {},
345
+ async readFile(filepath, opts) {
346
+ const path = normalizePath(filepath);
347
+ if (await knownAbsent(path)) throw enoent("open", filepath);
348
+ const data = await store.get(toKey$1(path));
349
+ if (data === null) throw enoent("open", filepath);
350
+ return resolveEncoding(opts) === "utf8" ? textDecoder.decode(data) : data;
351
+ },
352
+ async writeFile(filepath, data, _opts) {
353
+ const path = normalizePath(filepath);
354
+ if (useLooseHints) {
355
+ const scope = looseScope(path);
356
+ if (scope !== null) looseHints.set(scope, "present");
357
+ }
358
+ const bytes = typeof data === "string" ? textEncoder.encode(data) : data;
359
+ await store.put(toKey$1(path), bytes);
360
+ },
361
+ async unlink(filepath) {
362
+ const k = toKey$1(normalizePath(filepath));
363
+ if (await store.head(k) === null) throw enoent("unlink", filepath);
364
+ await store.delete(k);
365
+ },
366
+ async readdir(dirpath) {
367
+ const k = toKey$1(normalizePath(dirpath));
368
+ const isRoot = k === prefix || k === "";
369
+ const listPrefix = isRoot && k === "" ? "" : `${k}/`;
370
+ const { objects, prefixes } = await store.list(listPrefix, { delimiter: "/" });
371
+ if (objects.length === 0 && prefixes.length === 0) {
372
+ if (!isRoot && await store.head(k) !== null) throw enotdir("scandir", dirpath);
373
+ if (!isRoot) throw enoent("scandir", dirpath);
374
+ }
375
+ const names = objects.map((o) => o.key.slice(listPrefix.length));
376
+ const dirNames = prefixes.map((p) => p.slice(listPrefix.length).replace(/\/$/, ""));
377
+ return [...names, ...dirNames].sort();
378
+ },
379
+ async mkdir(_dirpath, _opts) {},
380
+ async rmdir(dirpath) {
381
+ const k = toKey$1(normalizePath(dirpath));
382
+ const { objects, prefixes } = await store.list(`${k}/`, { limit: 1 });
383
+ if (objects.length > 0 || prefixes.length > 0) throw enotempty("rmdir", dirpath);
384
+ },
385
+ stat: (filepath) => stat(filepath, "stat"),
386
+ lstat: (filepath) => stat(filepath, "lstat"),
387
+ async readlink(filepath) {
388
+ throw enoent("readlink", filepath);
389
+ },
390
+ async symlink(_target, filepath) {
391
+ throw eperm("symlink", filepath);
392
+ },
393
+ async chmod(_filepath, _mode) {}
394
+ };
395
+ async function detectLooseObjects(gitdir) {
396
+ if (!useLooseHints) return;
397
+ const scope = normalizePath(gitdir);
398
+ if (looseHints.has(scope)) return;
399
+ try {
400
+ const { objects } = await store.list(`${toKey$1(scope)}/objects/`, { limit: 1 });
401
+ const first = objects[0]?.key;
402
+ const hint = first !== void 0 && parseLooseObjectPath(first) !== null ? "present" : "none";
403
+ looseHints.set(scope, hint);
404
+ onNote?.(`loose objects ${hint} under ${scope}`);
405
+ } catch {}
406
+ }
407
+ async function prefetchPacks(gitdir, prefetchOptions) {
408
+ const maxPacks = prefetchOptions?.maxPacks ?? 30;
409
+ const packDir = `${normalizePath(gitdir)}/objects/pack`;
410
+ const entries = parsePackDirectory(await promises.readdir(packDir).catch(() => [])).filter((file) => READABLE_PACK_FILE_KINDS.has(file.kind)).map((file) => file.name);
411
+ if (entries.length > maxPacks * 2) {
412
+ await detectLooseObjects(gitdir);
413
+ return;
414
+ }
415
+ await Promise.all([detectLooseObjects(gitdir), ...entries.map((name) => promises.readFile(`${packDir}/${name}`).catch(() => void 0))]);
416
+ }
417
+ async function listFilesRecursively(dirpath) {
418
+ const directory = toKey$1(normalizePath(dirpath));
419
+ const listPrefix = directory === "" ? "" : `${directory}/`;
420
+ const { objects } = await store.list(listPrefix);
421
+ return objects.map((object) => object.key.slice(listPrefix.length)).filter((path) => path.length > 0).sort();
422
+ }
423
+ function registerGitdir(gitdir, facts) {
424
+ registeredGitdirs.set(normalizePath(gitdir), new Set(facts?.absentFiles ?? []));
425
+ }
426
+ function invalidate(pathPrefix) {
427
+ const normalized = normalizePath(pathPrefix);
428
+ for (const dir of registeredGitdirs.keys()) if (dir.startsWith(normalized)) registeredGitdirs.delete(dir);
429
+ for (const scope of looseHints.keys()) if (scope.startsWith(normalized)) looseHints.delete(scope);
430
+ store.invalidate?.(toKey$1(normalized));
431
+ }
432
+ return {
433
+ promises,
434
+ listFilesRecursively,
435
+ detectLooseObjects,
436
+ prefetchPacks,
437
+ invalidate,
438
+ registerGitdir
439
+ };
502
440
  }
503
-
504
- // src/prefix.ts
505
- var BATCH = 20;
441
+ //#endregion
442
+ //#region src/prefix.ts
443
+ const BATCH = 20;
506
444
  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
- }
445
+ for (let i = 0; i < items.length; i += BATCH) await Promise.all(items.slice(i, i + BATCH).map(fn));
510
446
  }
447
+ /**
448
+ * Delete every object under `prefix` (one paginated LIST, then batched
449
+ * deletes). Returns the number of objects deleted. Callers serialise against
450
+ * concurrent writers (e.g. a repository lock) and invalidate any cache
451
+ * layered above `store`.
452
+ */
511
453
  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;
454
+ const { objects } = await store.list(prefix);
455
+ await inBatches(objects, (object) => store.delete(object.key));
456
+ return objects.length;
515
457
  }
458
+ /**
459
+ * Move every object under `from` to the same relative key under `to`: copy
460
+ * everything first, then delete the old keys only once every copy succeeded,
461
+ * so a failed copy leaves the source untouched (a partial destination may
462
+ * remain; call {@link deletePrefix} on it to clean up). Returns the number of
463
+ * objects moved. `from` and `to` must not overlap.
464
+ */
516
465
  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;
466
+ if (from.startsWith(to) || to.startsWith(from)) throw new Error(`movePrefix: prefixes overlap (${from} -> ${to})`);
467
+ const copy = options.copy ?? (async (fromKey, toKey) => {
468
+ const data = await store.get(fromKey);
469
+ if (data) await store.put(toKey, data);
470
+ });
471
+ const { objects } = await store.list(from);
472
+ await inBatches(objects, (object) => copy(object.key, to + object.key.slice(from.length)));
473
+ await inBatches(objects, (object) => store.delete(object.key));
474
+ return objects.length;
531
475
  }
532
-
533
- // src/retry.ts
476
+ //#endregion
477
+ //#region src/retry.ts
478
+ /**
479
+ * Thrown instead of calling the backend while the circuit breaker is open.
480
+ * Carries `code: "EUNAVAILABLE"` so callers can map it to a 503.
481
+ */
534
482
  var CircuitOpenError = class extends Error {
535
- code = "EUNAVAILABLE";
536
- constructor() {
537
- super("Circuit breaker is open, object store unavailable");
538
- this.name = "CircuitOpenError";
539
- }
483
+ code = "EUNAVAILABLE";
484
+ constructor() {
485
+ super("Circuit breaker is open, object store unavailable");
486
+ this.name = "CircuitOpenError";
487
+ }
540
488
  };
541
- var RETRYABLE_NAMES = /* @__PURE__ */ new Set([
542
- "TimeoutError",
543
- "RequestTimeout",
544
- "RequestTimeoutException",
545
- "SlowDown",
546
- "ThrottlingException",
547
- "TooManyRequestsException"
489
+ const RETRYABLE_NAMES = /* @__PURE__ */ new Set([
490
+ "TimeoutError",
491
+ "RequestTimeout",
492
+ "RequestTimeoutException",
493
+ "SlowDown",
494
+ "ThrottlingException",
495
+ "TooManyRequestsException"
548
496
  ]);
549
- var RETRYABLE_CODES = /* @__PURE__ */ new Set([
550
- "ECONNRESET",
551
- "ECONNREFUSED",
552
- "EPIPE",
553
- "ETIMEDOUT",
554
- "ENOTFOUND",
555
- "EAI_AGAIN",
556
- "EPROTO"
497
+ const RETRYABLE_CODES = /* @__PURE__ */ new Set([
498
+ "ECONNRESET",
499
+ "ECONNREFUSED",
500
+ "EPIPE",
501
+ "ETIMEDOUT",
502
+ "ENOTFOUND",
503
+ "EAI_AGAIN",
504
+ "EPROTO"
557
505
  ]);
506
+ /**
507
+ * The default retry classifier: network faults, timeouts, throttling and HTTP
508
+ * 5xx/429 (including `$metadata.httpStatusCode`-shaped errors). `fetch`
509
+ * reports a dropped connection as a bare `TypeError`, which counts too.
510
+ * Compose it in a custom `isRetryable`.
511
+ */
558
512
  function isRetryableError(error) {
559
- if (error instanceof TypeError) return true;
560
- if (typeof error !== "object" || error === null) return false;
561
- const err = error;
562
- if (err.name !== void 0 && RETRYABLE_NAMES.has(err.name)) return true;
563
- if (err.code !== void 0 && RETRYABLE_CODES.has(err.code)) return true;
564
- const status = err.$metadata?.httpStatusCode;
565
- return status !== void 0 && (status >= 500 || status === 429);
513
+ if (error instanceof TypeError) return true;
514
+ if (typeof error !== "object" || error === null) return false;
515
+ const err = error;
516
+ if (err.name !== void 0 && RETRYABLE_NAMES.has(err.name)) return true;
517
+ if (err.code !== void 0 && RETRYABLE_CODES.has(err.code)) return true;
518
+ const status = err.$metadata?.httpStatusCode;
519
+ return status !== void 0 && (status >= 500 || status === 429);
566
520
  }
567
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
521
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
522
+ /**
523
+ * Wrap an {@link ObjectStore} with retries (exponential backoff + jitter) and
524
+ * an optional per-instance circuit breaker.
525
+ *
526
+ * Place this decorator closest to the network store, underneath any cache:
527
+ * the cache then never stores transient failures, and callers coalesced onto
528
+ * one request share a single retried attempt.
529
+ */
568
530
  function createRetryStore(store, options = {}) {
569
- const retries = options.retries ?? 3;
570
- const initialDelayMs = options.initialDelayMs ?? 100;
571
- const maxDelayMs = options.maxDelayMs ?? 5e3;
572
- const jitter = options.jitter ?? 0.3;
573
- const isRetryable = options.isRetryable ?? isRetryableError;
574
- const breaker = options.breaker === false ? null : {
575
- threshold: options.breaker?.threshold ?? 5,
576
- resetMs: options.breaker?.resetMs ?? 3e4
577
- };
578
- let failures = 0;
579
- let lastFailureAt = 0;
580
- let state = "closed";
581
- let probing = false;
582
- async function guarded(fn) {
583
- if (breaker === null) return fn();
584
- let isProbe = false;
585
- if (state !== "closed") {
586
- if (probing || state === "open" && Date.now() - lastFailureAt < breaker.resetMs) {
587
- throw new CircuitOpenError();
588
- }
589
- state = "half-open";
590
- probing = true;
591
- isProbe = true;
592
- }
593
- try {
594
- const result = await fn();
595
- failures = 0;
596
- state = "closed";
597
- return result;
598
- } catch (error) {
599
- if (isRetryable(error)) {
600
- failures++;
601
- lastFailureAt = Date.now();
602
- if (failures >= breaker.threshold || isProbe) state = "open";
603
- } else if (isProbe) {
604
- failures = 0;
605
- state = "closed";
606
- }
607
- throw error;
608
- } finally {
609
- if (isProbe) probing = false;
610
- }
611
- }
612
- async function run(op, key, fn) {
613
- let lastError;
614
- for (let attempt = 0; attempt <= retries; attempt++) {
615
- try {
616
- return await guarded(fn);
617
- } catch (error) {
618
- lastError = error;
619
- if (error instanceof CircuitOpenError) throw error;
620
- if (!isRetryable(error) || attempt === retries) throw error;
621
- const base = Math.min(initialDelayMs * 2 ** attempt, maxDelayMs);
622
- const delayMs = Math.round(base + Math.random() * base * jitter);
623
- options.onRetry?.({ key, op, attempt: attempt + 1, delayMs });
624
- await sleep(delayMs);
625
- }
626
- }
627
- throw lastError;
628
- }
629
- const range = store.getRange?.bind(store);
630
- return {
631
- get: (key) => run("get", key, () => store.get(key)),
632
- put: (key, data) => run("put", key, () => store.put(key, data)),
633
- delete: (key) => run("delete", key, () => store.delete(key)),
634
- head: (key) => run("head", key, () => store.head(key)),
635
- list: (prefix, listOptions) => run("list", prefix, () => store.list(prefix, listOptions)),
636
- ...range ? {
637
- getRange: (key, start, end) => run("getRange", key, () => range(key, start, end))
638
- } : {}
639
- };
531
+ const retries = options.retries ?? 3;
532
+ const initialDelayMs = options.initialDelayMs ?? 100;
533
+ const maxDelayMs = options.maxDelayMs ?? 5e3;
534
+ const jitter = options.jitter ?? .3;
535
+ const isRetryable = options.isRetryable ?? isRetryableError;
536
+ const breaker = options.breaker === false ? null : {
537
+ threshold: options.breaker?.threshold ?? 5,
538
+ resetMs: options.breaker?.resetMs ?? 3e4
539
+ };
540
+ let failures = 0;
541
+ let lastFailureAt = 0;
542
+ let state = "closed";
543
+ let probing = false;
544
+ /**
545
+ * Consecutive transient failures open the circuit; any success closes it
546
+ * and resets the count. Non-retryable errors (a 403, a malformed request)
547
+ * say nothing about the backend's health and never count. Once the reset
548
+ * window passes, exactly one request probes the backend (half-open);
549
+ * everything else keeps failing fast until that probe settles.
550
+ */
551
+ async function guarded(fn) {
552
+ if (breaker === null) return fn();
553
+ let isProbe = false;
554
+ if (state !== "closed") {
555
+ if (probing || state === "open" && Date.now() - lastFailureAt < breaker.resetMs) throw new CircuitOpenError();
556
+ state = "half-open";
557
+ probing = true;
558
+ isProbe = true;
559
+ }
560
+ try {
561
+ const result = await fn();
562
+ failures = 0;
563
+ state = "closed";
564
+ return result;
565
+ } catch (error) {
566
+ if (isRetryable(error)) {
567
+ failures++;
568
+ lastFailureAt = Date.now();
569
+ if (failures >= breaker.threshold || isProbe) state = "open";
570
+ } else if (isProbe) {
571
+ failures = 0;
572
+ state = "closed";
573
+ }
574
+ throw error;
575
+ } finally {
576
+ if (isProbe) probing = false;
577
+ }
578
+ }
579
+ async function run(op, key, fn) {
580
+ let lastError;
581
+ for (let attempt = 0; attempt <= retries; attempt++) try {
582
+ return await guarded(fn);
583
+ } catch (error) {
584
+ lastError = error;
585
+ if (error instanceof CircuitOpenError) throw error;
586
+ if (!isRetryable(error) || attempt === retries) throw error;
587
+ const base = Math.min(initialDelayMs * 2 ** attempt, maxDelayMs);
588
+ const delayMs = Math.round(base + Math.random() * base * jitter);
589
+ options.onRetry?.({
590
+ key,
591
+ op,
592
+ attempt: attempt + 1,
593
+ delayMs
594
+ });
595
+ await sleep(delayMs);
596
+ }
597
+ throw lastError;
598
+ }
599
+ const range = store.getRange?.bind(store);
600
+ return {
601
+ get: (key) => run("get", key, () => store.get(key)),
602
+ put: (key, data) => run("put", key, () => store.put(key, data)),
603
+ delete: (key) => run("delete", key, () => store.delete(key)),
604
+ head: (key) => run("head", key, () => store.head(key)),
605
+ list: (prefix, listOptions) => run("list", prefix, () => store.list(prefix, listOptions)),
606
+ ...range ? { getRange: (key, start, end) => run("getRange", key, () => range(key, start, end)) } : {}
607
+ };
640
608
  }
641
- export {
642
- CircuitOpenError,
643
- FsError,
644
- GitAuthenticationError,
645
- GitAuthorizationError,
646
- GitConflictError,
647
- GitError,
648
- GitInvalidRequestError,
649
- GitObjectNotFoundError,
650
- GitPathNotFoundError,
651
- GitProtocolError,
652
- GitRateLimitError,
653
- GitRefNotFoundError,
654
- GitRepositoryNotFoundError,
655
- MemoryObjectStore,
656
- PACK_FILE_KINDS,
657
- READABLE_PACK_FILE_KINDS,
658
- classifyGitdirPath,
659
- concat,
660
- createCachedStore,
661
- createGitFs,
662
- createRetryStore,
663
- decodeAscii,
664
- decodeUtf8,
665
- deflate,
666
- deletePrefix,
667
- encodeUtf8,
668
- formatErrorResponse,
669
- fromHex,
670
- getObjectSize,
671
- hasNullByte,
672
- isFullSha,
673
- isMutableGitdirEntry,
674
- isNotFoundError,
675
- isRetryableError,
676
- isSafeBranchName,
677
- isSafeFullRefName,
678
- isSafeRefName,
679
- isSafeRepoPath,
680
- isSafeTreePath,
681
- movePrefix,
682
- packFileName,
683
- parseLooseObjectPath,
684
- parsePackDirectory,
685
- parsePackFileName,
686
- qualifyBranchRef,
687
- readBlobContent,
688
- sha1,
689
- toBase64,
690
- toHex
691
- };
609
+ //#endregion
610
+ export { CircuitOpenError, FsError, GitAuthenticationError, GitAuthorizationError, GitConflictError, GitError, GitInvalidRequestError, GitObjectNotFoundError, GitPathNotFoundError, GitProtocolError, GitRateLimitError, GitRefNotFoundError, GitRepositoryNotFoundError, MemoryObjectStore, PACK_FILE_KINDS, READABLE_PACK_FILE_KINDS, classifyGitdirPath, concat, createCachedStore, createGitFs, createRetryStore, decodeAscii, decodeUtf8, deflate, deletePrefix, encodeUtf8, formatErrorResponse, fromHex, getObjectSize, hasNullByte, isAbsentError, isFullSha, isMutableGitdirEntry, isNotFoundError, isRetryableError, isSafeBranchName, isSafeFullRefName, isSafeRefName, isSafeRepoPath, isSafeTreePath, movePrefix, packFileName, parseLooseObjectPath, parsePackDirectory, parsePackFileName, qualifyBranchRef, readBlobContent, sha1, toBase64, toHex };
611
+
692
612
  //# sourceMappingURL=index.js.map