@nbtca/docs 0.3.2 → 0.3.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 +8 -0
- package/dist/client.js +59 -15
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ const matches = await docs.search('repair', { pathPrefix: 'repair' });
|
|
|
37
37
|
| `cacheTtlMs.dir` | `300000` | Directory and tree cache TTL |
|
|
38
38
|
| `cacheTtlMs.file` | `600000` | File cache TTL |
|
|
39
39
|
| `store` | none | Persistent cache, see below |
|
|
40
|
+
| `mirror` | none | Mirror base URL, see below |
|
|
40
41
|
|
|
41
42
|
### `docs.listDir(path?)`
|
|
42
43
|
|
|
@@ -73,6 +74,13 @@ tree under `tree` and file content under `blob-<sha>`, where `<sha>` is the Git
|
|
|
73
74
|
serves stored content only when its hash matches the blob id in the last known tree, so a changed
|
|
74
75
|
file is always refetched. Store errors and corrupt entries are ignored; eviction is up to the store.
|
|
75
76
|
|
|
77
|
+
### `mirror`
|
|
78
|
+
|
|
79
|
+
A base URL tried before GitHub by `listAll` and `getFile`, such as
|
|
80
|
+
`https://docs.nbtca.space/docs-api`. It serves `index.json` in the shape of GitHub's recursive tree
|
|
81
|
+
response and each file at `raw/<path>`. Any mirror failure, invalid or truncated index, or wait past
|
|
82
|
+
5 seconds falls back to GitHub. The GitHub token is never sent to the mirror.
|
|
83
|
+
|
|
76
84
|
### `docs.search(query, options?)`
|
|
77
85
|
|
|
78
86
|
Searches paths, titles, summaries, Markdown text, and semantic component attributes. Results are
|
package/dist/client.js
CHANGED
|
@@ -31,6 +31,7 @@ const SKIP = new Set([
|
|
|
31
31
|
'docs',
|
|
32
32
|
]);
|
|
33
33
|
const TREE_KEY = 'tree';
|
|
34
|
+
const MIRROR_TIMEOUT_MS = 5000;
|
|
34
35
|
const SHA = /^[0-9a-f]{40}$/;
|
|
35
36
|
const SEARCH_CONCURRENCY = 6;
|
|
36
37
|
const SEARCH_RESULT_LIMIT = 20;
|
|
@@ -189,6 +190,15 @@ function assertBranchRef(value) {
|
|
|
189
190
|
throw new TypeError('branch must be a valid Git ref');
|
|
190
191
|
}
|
|
191
192
|
}
|
|
193
|
+
function mirrorBase(value) {
|
|
194
|
+
if (value === undefined)
|
|
195
|
+
return undefined;
|
|
196
|
+
const url = URL.canParse(value) ? new URL(value) : undefined;
|
|
197
|
+
if (!url || !['https:', 'http:'].includes(url.protocol) || url.search || url.hash) {
|
|
198
|
+
throw new TypeError('mirror must be an http(s) URL without a query or fragment');
|
|
199
|
+
}
|
|
200
|
+
return url.href.replace(/\/+$/, '');
|
|
201
|
+
}
|
|
192
202
|
function cacheTtl(value, fallback, name) {
|
|
193
203
|
const ttl = value ?? fallback;
|
|
194
204
|
if (!Number.isFinite(ttl) || ttl < 0) {
|
|
@@ -279,6 +289,8 @@ export function createDocsClient(options = {}) {
|
|
|
279
289
|
const apiRepoUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
|
280
290
|
const rawRepoUrl = `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
|
281
291
|
const encodedBranch = encodeURIComponent(branch);
|
|
292
|
+
const mirror = mirrorBase(options.mirror);
|
|
293
|
+
let mirrorFailed = false;
|
|
282
294
|
const dirTtlMs = cacheTtl(options.cacheTtlMs?.dir, DEFAULTS.dirTtlMs, 'cacheTtlMs.dir');
|
|
283
295
|
const fileTtlMs = cacheTtl(options.cacheTtlMs?.file, DEFAULTS.fileTtlMs, 'cacheTtlMs.file');
|
|
284
296
|
const dirCache = new TtlCache(dirTtlMs, 30);
|
|
@@ -321,14 +333,14 @@ export function createDocsClient(options = {}) {
|
|
|
321
333
|
requestHeaders.Authorization = `Bearer ${token}`;
|
|
322
334
|
return requestHeaders;
|
|
323
335
|
}
|
|
324
|
-
async function withResponse(url, timeoutMs, consume) {
|
|
336
|
+
async function withResponse(url, timeoutMs, consume, requestHeaders = headers()) {
|
|
325
337
|
const ctrl = new AbortController();
|
|
326
338
|
const timer = setTimeout(() => {
|
|
327
339
|
ctrl.abort();
|
|
328
340
|
}, timeoutMs);
|
|
329
341
|
let response;
|
|
330
342
|
try {
|
|
331
|
-
response = await fetch(url, { signal: ctrl.signal, headers:
|
|
343
|
+
response = await fetch(url, { signal: ctrl.signal, headers: requestHeaders });
|
|
332
344
|
return await consume(response);
|
|
333
345
|
}
|
|
334
346
|
finally {
|
|
@@ -337,6 +349,36 @@ export function createDocsClient(options = {}) {
|
|
|
337
349
|
cancelUnusedResponseBody(response);
|
|
338
350
|
}
|
|
339
351
|
}
|
|
352
|
+
async function fromMirror(base, path, read) {
|
|
353
|
+
if (mirrorFailed)
|
|
354
|
+
return undefined;
|
|
355
|
+
try {
|
|
356
|
+
return await withResponse(`${base}/${path}`, MIRROR_TIMEOUT_MS, async (response) => {
|
|
357
|
+
if (response.status >= 500)
|
|
358
|
+
mirrorFailed = true;
|
|
359
|
+
return response.ok ? await read(response) : undefined;
|
|
360
|
+
}, {});
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
mirrorFailed = true;
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function keepTree(items, generation) {
|
|
368
|
+
if (generation === cacheGeneration) {
|
|
369
|
+
treeCache.set(TREE_KEY, copyItems(items));
|
|
370
|
+
if (store)
|
|
371
|
+
storeWrite(TREE_KEY, JSON.stringify(items));
|
|
372
|
+
}
|
|
373
|
+
return items;
|
|
374
|
+
}
|
|
375
|
+
async function keepFile(path, content, generation) {
|
|
376
|
+
if (generation === cacheGeneration)
|
|
377
|
+
fileCache.set(path, content);
|
|
378
|
+
if (store)
|
|
379
|
+
storeWrite(`blob-${await blobSha(content)}`, content);
|
|
380
|
+
return content;
|
|
381
|
+
}
|
|
340
382
|
function recoverFailure(cache, key, path, error, copy = (value) => value) {
|
|
341
383
|
const stale = cache.getStale(key);
|
|
342
384
|
if (stale !== undefined)
|
|
@@ -394,6 +436,14 @@ export function createDocsClient(options = {}) {
|
|
|
394
436
|
}
|
|
395
437
|
async function loadAll(generation) {
|
|
396
438
|
const key = TREE_KEY;
|
|
439
|
+
if (mirror !== undefined) {
|
|
440
|
+
const mirrored = await fromMirror(mirror, 'index.json', async (response) => {
|
|
441
|
+
const data = parseTreeResponse(await response.json());
|
|
442
|
+
return data.truncated ? undefined : filterTree(data.tree);
|
|
443
|
+
});
|
|
444
|
+
if (mirrored)
|
|
445
|
+
return keepTree(mirrored, generation);
|
|
446
|
+
}
|
|
397
447
|
const url = `${apiRepoUrl}/git/trees/${encodedBranch}?recursive=1`;
|
|
398
448
|
try {
|
|
399
449
|
return await withResponse(url, 20000, async (response) => {
|
|
@@ -410,13 +460,7 @@ export function createDocsClient(options = {}) {
|
|
|
410
460
|
return copyItems(stale);
|
|
411
461
|
throw new DocsFetchError('', null, 'GitHub truncated the repository tree (too many files) -- results would be incomplete');
|
|
412
462
|
}
|
|
413
|
-
|
|
414
|
-
if (generation === cacheGeneration) {
|
|
415
|
-
treeCache.set(key, copyItems(items));
|
|
416
|
-
if (store)
|
|
417
|
-
storeWrite(key, JSON.stringify(items));
|
|
418
|
-
}
|
|
419
|
-
return items;
|
|
463
|
+
return keepTree(filterTree(data.tree), generation);
|
|
420
464
|
});
|
|
421
465
|
}
|
|
422
466
|
catch (error) {
|
|
@@ -452,6 +496,11 @@ export function createDocsClient(options = {}) {
|
|
|
452
496
|
fileCache.set(path, stored);
|
|
453
497
|
return stored;
|
|
454
498
|
}
|
|
499
|
+
if (mirror !== undefined) {
|
|
500
|
+
const mirrored = await fromMirror(mirror, `raw/${encodePath(path)}`, (response) => response.text());
|
|
501
|
+
if (mirrored !== undefined)
|
|
502
|
+
return keepFile(path, mirrored, generation);
|
|
503
|
+
}
|
|
455
504
|
const url = `${rawRepoUrl}/${encodedBranch}/${encodePath(path)}`;
|
|
456
505
|
try {
|
|
457
506
|
return await withResponse(url, 15000, async (response) => {
|
|
@@ -461,12 +510,7 @@ export function createDocsClient(options = {}) {
|
|
|
461
510
|
return stale;
|
|
462
511
|
throw new DocsFetchError(path, response.status, `HTTP ${String(response.status)}`);
|
|
463
512
|
}
|
|
464
|
-
|
|
465
|
-
if (generation === cacheGeneration)
|
|
466
|
-
fileCache.set(path, content);
|
|
467
|
-
if (store)
|
|
468
|
-
storeWrite(`blob-${await blobSha(content)}`, content);
|
|
469
|
-
return content;
|
|
513
|
+
return keepFile(path, await response.text(), generation);
|
|
470
514
|
});
|
|
471
515
|
}
|
|
472
516
|
catch (error) {
|
package/dist/types.d.ts
CHANGED