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/README.md +5 -3
- package/dist/fetch-s3.js +137 -160
- package/dist/fetch-s3.js.map +1 -1
- package/dist/http.js +905 -849
- package/dist/http.js.map +1 -1
- package/dist/index.js +578 -658
- package/dist/index.js.map +1 -1
- package/dist/memory-M2cCuVF-.js +61 -0
- package/dist/memory-M2cCuVF-.js.map +1 -0
- package/dist/node.js +75 -71
- package/dist/node.js.map +1 -1
- package/dist/object-size-BJAsnZzw.js +191 -0
- package/dist/object-size-BJAsnZzw.js.map +1 -0
- package/dist/ops.js +662 -692
- package/dist/ops.js.map +1 -1
- package/dist/refs-Cb_NfKWI.js +550 -0
- package/dist/refs-Cb_NfKWI.js.map +1 -0
- package/dist/s3.js +111 -116
- package/dist/s3.js.map +1 -1
- package/dist/semaphore-BviNoQuJ.js +646 -0
- package/dist/semaphore-BviNoQuJ.js.map +1 -0
- package/dist/types/git-errors.d.ts +6 -0
- package/dist/types/http/repack.d.ts +7 -0
- package/dist/types/index.d.ts +1 -1
- package/package.json +18 -23
- package/dist/chunk-HLYCSNJ2.js +0 -53
- package/dist/chunk-HLYCSNJ2.js.map +0 -1
- package/dist/chunk-IAKZRWBZ.js +0 -598
- package/dist/chunk-IAKZRWBZ.js.map +0 -1
- package/dist/chunk-OKBQISUC.js +0 -442
- package/dist/chunk-OKBQISUC.js.map +0 -1
- package/dist/chunk-T5M5EYCZ.js +0 -143
- package/dist/chunk-T5M5EYCZ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,692 +1,612 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
} from "./
|
|
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
|
-
|
|
5
|
+
//#region src/cache.ts
|
|
6
|
+
const MISS = Symbol("miss");
|
|
52
7
|
function listEntrySize(entry) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
233
|
+
const normalized = normalizePath(filepath);
|
|
234
|
+
if (prefix === "") return normalized;
|
|
235
|
+
return normalized === "" ? prefix : `${prefix}/${normalized}`;
|
|
282
236
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
505
|
-
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region src/prefix.ts
|
|
443
|
+
const BATCH = 20;
|
|
506
444
|
async function inBatches(items, fn) {
|
|
507
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
483
|
+
code = "EUNAVAILABLE";
|
|
484
|
+
constructor() {
|
|
485
|
+
super("Circuit breaker is open, object store unavailable");
|
|
486
|
+
this.name = "CircuitOpenError";
|
|
487
|
+
}
|
|
540
488
|
};
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
489
|
+
const RETRYABLE_NAMES = /* @__PURE__ */ new Set([
|
|
490
|
+
"TimeoutError",
|
|
491
|
+
"RequestTimeout",
|
|
492
|
+
"RequestTimeoutException",
|
|
493
|
+
"SlowDown",
|
|
494
|
+
"ThrottlingException",
|
|
495
|
+
"TooManyRequestsException"
|
|
548
496
|
]);
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|