@nbtca/docs 0.3.1 → 0.3.2
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 +14 -1
- package/dist/client.js +87 -6
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ const matches = await docs.search('repair', { pathPrefix: 'repair' });
|
|
|
36
36
|
| `token` | `GITHUB_TOKEN` or `GH_TOKEN` | GitHub token |
|
|
37
37
|
| `cacheTtlMs.dir` | `300000` | Directory and tree cache TTL |
|
|
38
38
|
| `cacheTtlMs.file` | `600000` | File cache TTL |
|
|
39
|
+
| `store` | none | Persistent cache, see below |
|
|
39
40
|
|
|
40
41
|
### `docs.listDir(path?)`
|
|
41
42
|
|
|
@@ -48,7 +49,12 @@ Returns raw file content.
|
|
|
48
49
|
|
|
49
50
|
### `docs.listAll()`
|
|
50
51
|
|
|
51
|
-
Lists every Markdown file through GitHub's recursive tree API.
|
|
52
|
+
Lists every Markdown file through GitHub's recursive tree API. Each item carries its Git blob `sha`.
|
|
53
|
+
|
|
54
|
+
### `docs.peekAll()`
|
|
55
|
+
|
|
56
|
+
Returns the last known tree without a request: the one fetched in this process, else the one in
|
|
57
|
+
`store`, else `undefined`.
|
|
52
58
|
|
|
53
59
|
### `docs.listSections()`
|
|
54
60
|
|
|
@@ -60,6 +66,13 @@ Returns content with its route, section, title, summary, and semantic component
|
|
|
60
66
|
metadata covers `PageHero`, `FactStrip`, `LinkCard`, `Split`, `TimelineEntry`, and `Figure` without
|
|
61
67
|
imposing a renderer.
|
|
62
68
|
|
|
69
|
+
### `store`
|
|
70
|
+
|
|
71
|
+
An object with `read(key): string | undefined` and `write(key, value): void`. The client keeps the
|
|
72
|
+
tree under `tree` and file content under `blob-<sha>`, where `<sha>` is the Git blob id. `getFile`
|
|
73
|
+
serves stored content only when its hash matches the blob id in the last known tree, so a changed
|
|
74
|
+
file is always refetched. Store errors and corrupt entries are ignored; eviction is up to the store.
|
|
75
|
+
|
|
63
76
|
### `docs.search(query, options?)`
|
|
64
77
|
|
|
65
78
|
Searches paths, titles, summaries, Markdown text, and semantic component attributes. Results are
|
package/dist/client.js
CHANGED
|
@@ -30,6 +30,8 @@ const SKIP = new Set([
|
|
|
30
30
|
'README.md',
|
|
31
31
|
'docs',
|
|
32
32
|
]);
|
|
33
|
+
const TREE_KEY = 'tree';
|
|
34
|
+
const SHA = /^[0-9a-f]{40}$/;
|
|
33
35
|
const SEARCH_CONCURRENCY = 6;
|
|
34
36
|
const SEARCH_RESULT_LIMIT = 20;
|
|
35
37
|
function filterAndSort(raw) {
|
|
@@ -41,6 +43,7 @@ function filterAndSort(raw) {
|
|
|
41
43
|
name: item.name,
|
|
42
44
|
path: item.path,
|
|
43
45
|
type: item.type === 'dir' ? 'dir' : 'file',
|
|
46
|
+
...shaOf(item),
|
|
44
47
|
}))
|
|
45
48
|
.sort((a, b) => {
|
|
46
49
|
if (a.type !== b.type)
|
|
@@ -60,9 +63,40 @@ function filterTree(items) {
|
|
|
60
63
|
name: item.path.slice(item.path.lastIndexOf('/') + 1),
|
|
61
64
|
path: item.path,
|
|
62
65
|
type: 'file',
|
|
66
|
+
...shaOf(item),
|
|
63
67
|
}))
|
|
64
68
|
.sort((a, b) => a.path.localeCompare(b.path));
|
|
65
69
|
}
|
|
70
|
+
function shaOf(item) {
|
|
71
|
+
return typeof item.sha === 'string' && SHA.test(item.sha) ? { sha: item.sha } : {};
|
|
72
|
+
}
|
|
73
|
+
async function blobSha(content) {
|
|
74
|
+
const body = new TextEncoder().encode(content);
|
|
75
|
+
const header = new TextEncoder().encode(`blob ${String(body.length)}\0`);
|
|
76
|
+
const object = new Uint8Array(header.length + body.length);
|
|
77
|
+
object.set(header);
|
|
78
|
+
object.set(body, header.length);
|
|
79
|
+
const digest = new Uint8Array(await crypto.subtle.digest('SHA-1', object));
|
|
80
|
+
return Array.from(digest, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
81
|
+
}
|
|
82
|
+
function isDocItem(value) {
|
|
83
|
+
return (isRecord(value) &&
|
|
84
|
+
typeof value.name === 'string' &&
|
|
85
|
+
typeof value.path === 'string' &&
|
|
86
|
+
(value.type === 'file' || value.type === 'dir') &&
|
|
87
|
+
(value.sha === undefined || typeof value.sha === 'string'));
|
|
88
|
+
}
|
|
89
|
+
function parseStoredTree(value) {
|
|
90
|
+
if (value === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
try {
|
|
93
|
+
const items = JSON.parse(value);
|
|
94
|
+
return Array.isArray(items) && items.every(isDocItem) ? items : undefined;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
66
100
|
function copyItems(items) {
|
|
67
101
|
return items.map((item) => ({ ...item }));
|
|
68
102
|
}
|
|
@@ -253,7 +287,32 @@ export function createDocsClient(options = {}) {
|
|
|
253
287
|
const dirRequests = new Map();
|
|
254
288
|
const fileRequests = new Map();
|
|
255
289
|
const treeRequests = new Map();
|
|
290
|
+
const store = options.store;
|
|
291
|
+
let storedTree;
|
|
256
292
|
let cacheGeneration = 0;
|
|
293
|
+
function storeRead(key) {
|
|
294
|
+
try {
|
|
295
|
+
return store?.read(key);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function storeWrite(key, value) {
|
|
302
|
+
try {
|
|
303
|
+
store?.write(key, value);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function knownTree() {
|
|
310
|
+
const fetched = treeCache.getStale(TREE_KEY);
|
|
311
|
+
if (fetched)
|
|
312
|
+
return fetched;
|
|
313
|
+
storedTree ?? (storedTree = parseStoredTree(storeRead(TREE_KEY)));
|
|
314
|
+
return storedTree;
|
|
315
|
+
}
|
|
257
316
|
function headers() {
|
|
258
317
|
const requestHeaders = {
|
|
259
318
|
Accept: 'application/vnd.github.v3+json',
|
|
@@ -334,7 +393,7 @@ export function createDocsClient(options = {}) {
|
|
|
334
393
|
return shareRequest(dirRequests, path, () => loadDir(path, cacheGeneration)).then(copyItems);
|
|
335
394
|
}
|
|
336
395
|
async function loadAll(generation) {
|
|
337
|
-
const key =
|
|
396
|
+
const key = TREE_KEY;
|
|
338
397
|
const url = `${apiRepoUrl}/git/trees/${encodedBranch}?recursive=1`;
|
|
339
398
|
try {
|
|
340
399
|
return await withResponse(url, 20000, async (response) => {
|
|
@@ -352,8 +411,11 @@ export function createDocsClient(options = {}) {
|
|
|
352
411
|
throw new DocsFetchError('', null, 'GitHub truncated the repository tree (too many files) -- results would be incomplete');
|
|
353
412
|
}
|
|
354
413
|
const items = filterTree(data.tree);
|
|
355
|
-
if (generation === cacheGeneration)
|
|
414
|
+
if (generation === cacheGeneration) {
|
|
356
415
|
treeCache.set(key, copyItems(items));
|
|
416
|
+
if (store)
|
|
417
|
+
storeWrite(key, JSON.stringify(items));
|
|
418
|
+
}
|
|
357
419
|
return items;
|
|
358
420
|
});
|
|
359
421
|
}
|
|
@@ -364,16 +426,32 @@ export function createDocsClient(options = {}) {
|
|
|
364
426
|
}
|
|
365
427
|
}
|
|
366
428
|
function listAll() {
|
|
367
|
-
const
|
|
368
|
-
const hit = treeCache.get(key);
|
|
429
|
+
const hit = treeCache.get(TREE_KEY);
|
|
369
430
|
if (hit)
|
|
370
431
|
return Promise.resolve(copyItems(hit));
|
|
371
|
-
return shareRequest(treeRequests,
|
|
432
|
+
return shareRequest(treeRequests, TREE_KEY, () => loadAll(cacheGeneration)).then(copyItems);
|
|
433
|
+
}
|
|
434
|
+
function peekAll() {
|
|
435
|
+
const items = knownTree();
|
|
436
|
+
return items && copyItems(items);
|
|
372
437
|
}
|
|
373
438
|
async function listSections() {
|
|
374
439
|
return sectionsFromItems(await listAll());
|
|
375
440
|
}
|
|
441
|
+
async function loadStoredFile(path) {
|
|
442
|
+
const sha = knownTree()?.find((item) => item.path === path)?.sha;
|
|
443
|
+
if (sha === undefined)
|
|
444
|
+
return undefined;
|
|
445
|
+
const content = storeRead(`blob-${sha}`);
|
|
446
|
+
return content !== undefined && (await blobSha(content)) === sha ? content : undefined;
|
|
447
|
+
}
|
|
376
448
|
async function loadFile(path, generation) {
|
|
449
|
+
const stored = store && (await loadStoredFile(path));
|
|
450
|
+
if (stored !== undefined) {
|
|
451
|
+
if (generation === cacheGeneration)
|
|
452
|
+
fileCache.set(path, stored);
|
|
453
|
+
return stored;
|
|
454
|
+
}
|
|
377
455
|
const url = `${rawRepoUrl}/${encodedBranch}/${encodePath(path)}`;
|
|
378
456
|
try {
|
|
379
457
|
return await withResponse(url, 15000, async (response) => {
|
|
@@ -386,6 +464,8 @@ export function createDocsClient(options = {}) {
|
|
|
386
464
|
const content = await response.text();
|
|
387
465
|
if (generation === cacheGeneration)
|
|
388
466
|
fileCache.set(path, content);
|
|
467
|
+
if (store)
|
|
468
|
+
storeWrite(`blob-${await blobSha(content)}`, content);
|
|
389
469
|
return content;
|
|
390
470
|
});
|
|
391
471
|
}
|
|
@@ -451,6 +531,7 @@ export function createDocsClient(options = {}) {
|
|
|
451
531
|
}
|
|
452
532
|
function clear() {
|
|
453
533
|
cacheGeneration += 1;
|
|
534
|
+
storedTree = undefined;
|
|
454
535
|
dirCache.clear();
|
|
455
536
|
fileCache.clear();
|
|
456
537
|
treeCache.clear();
|
|
@@ -458,5 +539,5 @@ export function createDocsClient(options = {}) {
|
|
|
458
539
|
fileRequests.clear();
|
|
459
540
|
treeRequests.clear();
|
|
460
541
|
}
|
|
461
|
-
return { listDir, listAll, listSections, getFile, getDocument, search, clear };
|
|
542
|
+
return { listDir, listAll, peekAll, listSections, getFile, getDocument, search, clear };
|
|
462
543
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { createDocsClient } from './client.js';
|
|
2
2
|
export { parseDoc } from './content.js';
|
|
3
|
-
export type { DocComponent, DocItem, DocPage, DocSection, DocsClient, DocsClientOptions, DocsSearchOptions, DocsSearchResult, } from './types.js';
|
|
3
|
+
export type { DocComponent, DocItem, DocPage, DocSection, DocsClient, DocsClientOptions, DocsSearchOptions, DocsSearchResult, DocsStore, } from './types.js';
|
|
4
4
|
export { DocsFetchError } from './types.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export interface DocItem {
|
|
|
2
2
|
name: string;
|
|
3
3
|
path: string;
|
|
4
4
|
type: 'file' | 'dir';
|
|
5
|
+
sha?: string;
|
|
5
6
|
}
|
|
6
7
|
export interface DocComponent {
|
|
7
8
|
attributes: Readonly<Record<string, string | true>>;
|
|
@@ -36,6 +37,10 @@ export interface DocsSearchResult {
|
|
|
36
37
|
summary: string;
|
|
37
38
|
title: string;
|
|
38
39
|
}
|
|
40
|
+
export interface DocsStore {
|
|
41
|
+
read(key: string): string | undefined;
|
|
42
|
+
write(key: string, value: string): void;
|
|
43
|
+
}
|
|
39
44
|
export interface DocsClientOptions {
|
|
40
45
|
owner?: string;
|
|
41
46
|
repo?: string;
|
|
@@ -45,10 +50,12 @@ export interface DocsClientOptions {
|
|
|
45
50
|
dir?: number;
|
|
46
51
|
file?: number;
|
|
47
52
|
};
|
|
53
|
+
store?: DocsStore;
|
|
48
54
|
}
|
|
49
55
|
export interface DocsClient {
|
|
50
56
|
listDir(path?: string): Promise<DocItem[]>;
|
|
51
57
|
listAll(): Promise<DocItem[]>;
|
|
58
|
+
peekAll(): DocItem[] | undefined;
|
|
52
59
|
listSections(): Promise<DocSection[]>;
|
|
53
60
|
getFile(path: string): Promise<string>;
|
|
54
61
|
getDocument(path: string): Promise<DocPage>;
|