@hatk/hatk 0.0.1-alpha.9 → 0.0.1-alpha.92
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/adapter.d.ts +19 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +113 -0
- package/dist/backfill.d.ts +29 -2
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +142 -51
- package/dist/car.d.ts +20 -0
- package/dist/car.d.ts.map +1 -1
- package/dist/car.js +108 -0
- package/dist/cli.js +247 -1043
- package/dist/config.d.ts +145 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +61 -8
- package/dist/database/adapter-factory.d.ts +6 -0
- package/dist/database/adapter-factory.d.ts.map +1 -0
- package/dist/database/adapter-factory.js +20 -0
- package/dist/database/adapters/duckdb-search.d.ts +12 -0
- package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
- package/dist/database/adapters/duckdb-search.js +27 -0
- package/dist/database/adapters/duckdb.d.ts +25 -0
- package/dist/database/adapters/duckdb.d.ts.map +1 -0
- package/dist/database/adapters/duckdb.js +161 -0
- package/dist/database/adapters/sqlite-search.d.ts +16 -0
- package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
- package/dist/database/adapters/sqlite-search.js +76 -0
- package/dist/database/adapters/sqlite.d.ts +18 -0
- package/dist/database/adapters/sqlite.d.ts.map +1 -0
- package/dist/database/adapters/sqlite.js +88 -0
- package/dist/{db.d.ts → database/db.d.ts} +68 -6
- package/dist/database/db.d.ts.map +1 -0
- package/dist/database/db.js +1775 -0
- package/dist/database/dialect.d.ts +45 -0
- package/dist/database/dialect.d.ts.map +1 -0
- package/dist/database/dialect.js +72 -0
- package/dist/{fts.d.ts → database/fts.d.ts} +7 -0
- package/dist/database/fts.d.ts.map +1 -0
- package/dist/{fts.js → database/fts.js} +122 -34
- package/dist/database/index.d.ts +7 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +6 -0
- package/dist/database/ports.d.ts +50 -0
- package/dist/database/ports.d.ts.map +1 -0
- package/dist/database/ports.js +1 -0
- package/dist/{schema.d.ts → database/schema.d.ts} +16 -3
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/{schema.js → database/schema.js} +126 -44
- package/dist/dev-entry.d.ts +8 -0
- package/dist/dev-entry.d.ts.map +1 -0
- package/dist/dev-entry.js +127 -0
- package/dist/feeds.d.ts +26 -8
- package/dist/feeds.d.ts.map +1 -1
- package/dist/feeds.js +72 -16
- package/dist/hooks.d.ts +102 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +186 -0
- package/dist/hydrate.d.ts +49 -6
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +21 -20
- package/dist/indexer.d.ts +167 -3
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +556 -94
- package/dist/jetstream.d.ts +118 -0
- package/dist/jetstream.d.ts.map +1 -0
- package/dist/jetstream.js +225 -0
- package/dist/labels.d.ts +36 -0
- package/dist/labels.d.ts.map +1 -1
- package/dist/labels.js +71 -6
- package/dist/lex-types.d.ts +2 -0
- package/dist/lex-types.d.ts.map +1 -1
- package/dist/lexicon-resolve.d.ts.map +1 -1
- package/dist/lexicon-resolve.js +27 -112
- package/dist/lexicons/com/atproto/label/defs.json +75 -0
- package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
- package/dist/lexicons/com/atproto/repo/strongRef.json +21 -0
- package/dist/lexicons/dev/hatk/applyWrites.json +87 -0
- package/dist/lexicons/dev/hatk/createRecord.json +40 -0
- package/dist/lexicons/dev/hatk/createReport.json +48 -0
- package/dist/lexicons/dev/hatk/deleteRecord.json +25 -0
- package/dist/lexicons/dev/hatk/describeCollections.json +41 -0
- package/dist/lexicons/dev/hatk/describeFeeds.json +29 -0
- package/dist/lexicons/dev/hatk/describeLabels.json +45 -0
- package/dist/lexicons/dev/hatk/getFeed.json +30 -0
- package/dist/lexicons/dev/hatk/getPreferences.json +19 -0
- package/dist/lexicons/dev/hatk/getRecord.json +26 -0
- package/dist/lexicons/dev/hatk/getRecords.json +32 -0
- package/dist/lexicons/dev/hatk/putPreference.json +28 -0
- package/dist/lexicons/dev/hatk/putRecord.json +41 -0
- package/dist/lexicons/dev/hatk/searchRecords.json +32 -0
- package/dist/lexicons/dev/hatk/uploadBlob.json +23 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +29 -0
- package/dist/main.js +198 -75
- package/dist/mst.d.ts +15 -0
- package/dist/mst.d.ts.map +1 -1
- package/dist/mst.js +13 -0
- package/dist/oauth/db.d.ts +16 -1
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +74 -19
- package/dist/oauth/discovery.d.ts +10 -0
- package/dist/oauth/discovery.d.ts.map +1 -1
- package/dist/oauth/discovery.js +54 -2
- package/dist/oauth/server.d.ts +93 -1
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +416 -39
- package/dist/oauth/service-describe.d.ts +42 -0
- package/dist/oauth/service-describe.d.ts.map +1 -0
- package/dist/oauth/service-describe.js +108 -0
- package/dist/oauth/session.d.ts +32 -0
- package/dist/oauth/session.d.ts.map +1 -0
- package/dist/oauth/session.js +131 -0
- package/dist/opengraph.d.ts +10 -0
- package/dist/opengraph.d.ts.map +1 -1
- package/dist/opengraph.js +94 -42
- package/dist/pds-proxy.d.ts +82 -0
- package/dist/pds-proxy.d.ts.map +1 -0
- package/dist/pds-proxy.js +356 -0
- package/dist/private-collections.d.ts +13 -0
- package/dist/private-collections.d.ts.map +1 -0
- package/dist/private-collections.js +27 -0
- package/dist/push.d.ts +54 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +367 -0
- package/dist/renderer.d.ts +27 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +46 -0
- package/dist/resolve-hatk.d.ts +6 -0
- package/dist/resolve-hatk.d.ts.map +1 -0
- package/dist/resolve-hatk.js +20 -0
- package/dist/response.d.ts +16 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +69 -0
- package/dist/scanner.d.ts +21 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +88 -0
- package/dist/seed.d.ts +19 -0
- package/dist/seed.d.ts.map +1 -1
- package/dist/seed.js +43 -4
- package/dist/server-init.d.ts +8 -0
- package/dist/server-init.d.ts.map +1 -0
- package/dist/server-init.js +62 -0
- package/dist/server.d.ts +34 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1032 -649
- package/dist/setup.d.ts +28 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +50 -3
- package/dist/spaces/blob.d.ts +51 -0
- package/dist/spaces/blob.d.ts.map +1 -0
- package/dist/spaces/blob.js +111 -0
- package/dist/spaces/credential.d.ts +62 -0
- package/dist/spaces/credential.d.ts.map +1 -0
- package/dist/spaces/credential.js +184 -0
- package/dist/spaces/engine.d.ts +97 -0
- package/dist/spaces/engine.d.ts.map +1 -0
- package/dist/spaces/engine.js +559 -0
- package/dist/spaces/guard.d.ts +41 -0
- package/dist/spaces/guard.d.ts.map +1 -0
- package/dist/spaces/guard.js +79 -0
- package/dist/spaces/identity.d.ts +37 -0
- package/dist/spaces/identity.d.ts.map +1 -0
- package/dist/spaces/identity.js +88 -0
- package/dist/spaces/index.d.ts +55 -0
- package/dist/spaces/index.d.ts.map +1 -0
- package/dist/spaces/index.js +133 -0
- package/dist/spaces/notify.d.ts +51 -0
- package/dist/spaces/notify.d.ts.map +1 -0
- package/dist/spaces/notify.js +121 -0
- package/dist/spaces/records.d.ts +37 -0
- package/dist/spaces/records.d.ts.map +1 -0
- package/dist/spaces/records.js +63 -0
- package/dist/spaces/store.d.ts +51 -0
- package/dist/spaces/store.d.ts.map +1 -0
- package/dist/spaces/store.js +99 -0
- package/dist/spaces/uri.d.ts +60 -0
- package/dist/spaces/uri.d.ts.map +1 -0
- package/dist/spaces/uri.js +97 -0
- package/dist/spaces/verify.d.ts +39 -0
- package/dist/spaces/verify.d.ts.map +1 -0
- package/dist/spaces/verify.js +87 -0
- package/dist/spaces/viewer.d.ts +51 -0
- package/dist/spaces/viewer.d.ts.map +1 -0
- package/dist/spaces/viewer.js +167 -0
- package/dist/spaces/visibility.d.ts +57 -0
- package/dist/spaces/visibility.d.ts.map +1 -0
- package/dist/spaces/visibility.js +81 -0
- package/dist/templates/feed.tpl +18 -0
- package/dist/templates/hook.tpl +5 -0
- package/dist/templates/label.tpl +15 -0
- package/dist/templates/og.tpl +17 -0
- package/dist/templates/seed.tpl +11 -0
- package/dist/templates/setup.tpl +5 -0
- package/dist/templates/test-feed.tpl +19 -0
- package/dist/templates/test-xrpc.tpl +19 -0
- package/dist/templates/xrpc.tpl +53 -0
- package/dist/test.d.ts +3 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +60 -36
- package/dist/views.js +1 -1
- package/dist/vite-plugin.d.ts +1 -1
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +254 -66
- package/dist/xrpc.d.ts +101 -12
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +255 -55
- package/package.json +21 -9
- package/public/admin-auth.js +271 -306
- package/public/admin.html +177 -61
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js +0 -1361
- package/dist/fts.d.ts.map +0 -1
- package/dist/oauth/hooks.d.ts +0 -10
- package/dist/oauth/hooks.d.ts.map +0 -1
- package/dist/oauth/hooks.js +0 -40
- package/dist/schema.d.ts.map +0 -1
- package/dist/test-browser.d.ts +0 -14
- package/dist/test-browser.d.ts.map +0 -1
- package/dist/test-browser.js +0 -26
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Convert a Node.js IncomingMessage to a Web Standard Request.
|
|
4
|
+
*/
|
|
5
|
+
export declare function toRequest(req: IncomingMessage, base: string): Request;
|
|
6
|
+
/**
|
|
7
|
+
* Pipe a Web Standard Response back to a Node.js ServerResponse.
|
|
8
|
+
*/
|
|
9
|
+
export declare function sendResponse(res: ServerResponse, response: Response): Promise<void>;
|
|
10
|
+
/** Routes handled by hatk — everything else can fall through to a framework handler. */
|
|
11
|
+
export declare const HATK_ROUTES: string[];
|
|
12
|
+
export declare function isHatkRoute(pathname: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Create a Node.js HTTP server from a Web Standard fetch handler.
|
|
15
|
+
* If a fallback Node middleware is provided, non-hatk routes are sent to it
|
|
16
|
+
* (e.g. SvelteKit's handler from build/handler.js).
|
|
17
|
+
*/
|
|
18
|
+
export declare function serve(handler: (request: Request) => Promise<Response>, port: number, base?: string, fallback?: (req: IncomingMessage, res: ServerResponse, next: () => void) => void): import("node:http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
19
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAgB,MAAM,WAAW,CAAA;AAEnF;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CA0BrE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBzF;AAED,wFAAwF;AACxF,eAAO,MAAM,WAAW,UAmBvB,CAAA;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CACnB,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAChD,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,EACb,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI,6EA4BjF"}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Convert a Node.js IncomingMessage to a Web Standard Request.
|
|
4
|
+
*/
|
|
5
|
+
export function toRequest(req, base) {
|
|
6
|
+
const url = new URL(req.url, base);
|
|
7
|
+
const headers = new Headers();
|
|
8
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
9
|
+
if (value) {
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
for (const v of value)
|
|
12
|
+
headers.append(key, v);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
headers.set(key, value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const init = {
|
|
20
|
+
method: req.method,
|
|
21
|
+
headers,
|
|
22
|
+
};
|
|
23
|
+
// GET and HEAD requests cannot have a body
|
|
24
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
25
|
+
// @ts-expect-error — Node.js streams are valid body sources
|
|
26
|
+
init.body = req;
|
|
27
|
+
init.duplex = 'half';
|
|
28
|
+
}
|
|
29
|
+
return new Request(url.href, init);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pipe a Web Standard Response back to a Node.js ServerResponse.
|
|
33
|
+
*/
|
|
34
|
+
export async function sendResponse(res, response) {
|
|
35
|
+
const rawHeaders = [];
|
|
36
|
+
response.headers.forEach((value, name) => {
|
|
37
|
+
rawHeaders.push(name, value);
|
|
38
|
+
});
|
|
39
|
+
res.writeHead(response.status, rawHeaders);
|
|
40
|
+
if (!response.body) {
|
|
41
|
+
res.end();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const reader = response.body.getReader();
|
|
45
|
+
try {
|
|
46
|
+
while (true) {
|
|
47
|
+
const { done, value } = await reader.read();
|
|
48
|
+
if (done)
|
|
49
|
+
break;
|
|
50
|
+
res.write(value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
reader.releaseLock();
|
|
55
|
+
res.end();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Routes handled by hatk — everything else can fall through to a framework handler. */
|
|
59
|
+
export const HATK_ROUTES = [
|
|
60
|
+
'/xrpc/',
|
|
61
|
+
'/blob/', // dev image proxy — see blobUrl in xrpc.ts
|
|
62
|
+
'/space-blob', // a blob inside a permissioned space, served per viewer
|
|
63
|
+
'/.well-known/did.json', // this instance's own DID, for space notice delivery
|
|
64
|
+
'/oauth/',
|
|
65
|
+
'/oauth-client-metadata.json',
|
|
66
|
+
'/.well-known/oauth-authorization-server',
|
|
67
|
+
'/.well-known/oauth-protected-resource',
|
|
68
|
+
'/og/',
|
|
69
|
+
'/admin',
|
|
70
|
+
'/repos',
|
|
71
|
+
'/info/',
|
|
72
|
+
'/_health',
|
|
73
|
+
'/robots.txt',
|
|
74
|
+
'/auth/logout',
|
|
75
|
+
'/auth/switch',
|
|
76
|
+
'/auth/accounts',
|
|
77
|
+
'/__dev/',
|
|
78
|
+
];
|
|
79
|
+
export function isHatkRoute(pathname) {
|
|
80
|
+
return HATK_ROUTES.some((r) => pathname.startsWith(r) || pathname === r);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create a Node.js HTTP server from a Web Standard fetch handler.
|
|
84
|
+
* If a fallback Node middleware is provided, non-hatk routes are sent to it
|
|
85
|
+
* (e.g. SvelteKit's handler from build/handler.js).
|
|
86
|
+
*/
|
|
87
|
+
export function serve(handler, port, base, fallback) {
|
|
88
|
+
const origin = base || `http://localhost:${port}`;
|
|
89
|
+
const server = createServer(async (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const url = new URL(req.url, origin);
|
|
92
|
+
// If we have a fallback (e.g. SvelteKit) and this isn't a hatk route, skip hatk
|
|
93
|
+
if (fallback && !isHatkRoute(url.pathname)) {
|
|
94
|
+
fallback(req, res, () => {
|
|
95
|
+
res.writeHead(404);
|
|
96
|
+
res.end('Not found');
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const request = toRequest(req, origin);
|
|
101
|
+
const response = await handler(request);
|
|
102
|
+
await sendResponse(res, response);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (!res.headersSent) {
|
|
106
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
107
|
+
}
|
|
108
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
server.listen(port);
|
|
112
|
+
return server;
|
|
113
|
+
}
|
package/dist/backfill.d.ts
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The collections whose local rows a full re-import may purge before
|
|
3
|
+
* re-inserting from the repo CAR.
|
|
4
|
+
*
|
|
5
|
+
* Private collections are AppView-authoritative and never present in a repo:
|
|
6
|
+
* purging their rows would destroy data the re-import can never restore (this
|
|
7
|
+
* happened — a login-triggered backfill wiped a deployment's activity rows).
|
|
8
|
+
* Exported for tests.
|
|
9
|
+
*/
|
|
10
|
+
export declare function purgeableCollections(collections: Iterable<string>): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Whether a repo with this stored status should be queued for backfill.
|
|
13
|
+
*
|
|
14
|
+
* Takendown repos are excluded: re-backfilling one ends in
|
|
15
|
+
* `setRepoStatus(did, 'active')`, which would silently reinstate an account an
|
|
16
|
+
* admin took down — takedowns must survive every redeploy. Reinstatement goes
|
|
17
|
+
* through `/admin/reverse-takedown` only. Exported for tests.
|
|
18
|
+
*/
|
|
19
|
+
export declare function backfillEligible(status: string | null): boolean;
|
|
1
20
|
import type { BackfillConfig } from './config.ts';
|
|
2
21
|
/** Options passed to {@link runBackfill}. */
|
|
3
22
|
interface BackfillOpts {
|
|
4
23
|
/** Base URL of the relay or PDS to enumerate repos from (e.g. `wss://bsky.network`). */
|
|
5
24
|
pdsUrl: string;
|
|
25
|
+
/** Further relays/PDSes to enumerate — the HTTP side of `config.relays`. */
|
|
26
|
+
extraPdsUrls?: string[];
|
|
6
27
|
/** PLC directory URL used to resolve `did:plc` identifiers (e.g. `https://plc.directory`). */
|
|
7
28
|
plcUrl: string;
|
|
8
29
|
/** AT Protocol collection NSIDs to index (e.g. `app.bsky.feed.post`). */
|
|
9
30
|
collections: Set<string>;
|
|
10
|
-
/** Backfill behavior settings from `config.
|
|
31
|
+
/** Backfill behavior settings from `hatk.config.ts`. */
|
|
11
32
|
config: BackfillConfig;
|
|
12
33
|
}
|
|
34
|
+
/** Point DID resolution at a PLC directory. `runBackfill` does this too; call it
|
|
35
|
+
* at boot so resolvers that run before the first backfill have somewhere to go. */
|
|
36
|
+
export declare function configurePlc(url: string): void;
|
|
37
|
+
/** A repo's PDS endpoint, cached for the process. For serving blobs, where a
|
|
38
|
+
* stale answer after a migration costs one broken image, not a wrong row. */
|
|
39
|
+
export declare function pdsFor(did: string): Promise<string>;
|
|
13
40
|
/**
|
|
14
41
|
* Downloads and indexes a single user's repo via `com.atproto.sync.getRepo`.
|
|
15
42
|
*
|
|
@@ -65,6 +92,6 @@ export declare function backfillRepo(did: string, collections: Set<string>, fetc
|
|
|
65
92
|
* })
|
|
66
93
|
* ```
|
|
67
94
|
*/
|
|
68
|
-
export declare function runBackfill(opts: BackfillOpts): Promise<
|
|
95
|
+
export declare function runBackfill(opts: BackfillOpts): Promise<number>;
|
|
69
96
|
export {};
|
|
70
97
|
//# sourceMappingURL=backfill.d.ts.map
|
package/dist/backfill.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backfill.d.ts","sourceRoot":"","sources":["../src/backfill.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"backfill.d.ts","sourceRoot":"","sources":["../src/backfill.ts"],"names":[],"mappings":"AAkBA;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAE5E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAE/D;AAGD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,6CAA6C;AAC7C,UAAU,YAAY;IACpB,wFAAwF;IACxF,MAAM,EAAE,MAAM,CAAA;IACd,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,8FAA8F;IAC9F,MAAM,EAAE,MAAM,CAAA;IACd,yEAAyE;IACzE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,wDAAwD;IACxD,MAAM,EAAE,cAAc,CAAA;CACvB;AAWD;oFACoF;AACpF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAE9C;AAID;8EAC8E;AAC9E,wBAAsB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMzD;AAsFD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA+K/G;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAsIrE"}
|
package/dist/backfill.js
CHANGED
|
@@ -1,16 +1,59 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { noteReferencedRepos } from "./indexer.js";
|
|
2
|
+
import { parseCarStream } from "./car.js";
|
|
2
3
|
import { cborDecode } from "./cbor.js";
|
|
3
4
|
import { walkMst } from "./mst.js";
|
|
4
|
-
import { setRepoStatus, getRepoStatus, getRepoRetryInfo, listRetryEligibleRepos, listPendingRepos, querySQL, runSQL, getSchema, bulkInsertRecords, } from "./db.js";
|
|
5
|
+
import { setRepoStatus, getRepoStatus, getRepoRev, getRepoRetryInfo, listRetryEligibleRepos, listPendingRepos, querySQL, runSQL, getSchema, bulkInsertRecords, } from "./database/db.js";
|
|
6
|
+
import { isPrivateCollection } from "./private-collections.js";
|
|
7
|
+
/**
|
|
8
|
+
* The collections whose local rows a full re-import may purge before
|
|
9
|
+
* re-inserting from the repo CAR.
|
|
10
|
+
*
|
|
11
|
+
* Private collections are AppView-authoritative and never present in a repo:
|
|
12
|
+
* purging their rows would destroy data the re-import can never restore (this
|
|
13
|
+
* happened — a login-triggered backfill wiped a deployment's activity rows).
|
|
14
|
+
* Exported for tests.
|
|
15
|
+
*/
|
|
16
|
+
export function purgeableCollections(collections) {
|
|
17
|
+
return [...collections].filter((c) => !isPrivateCollection(c));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether a repo with this stored status should be queued for backfill.
|
|
21
|
+
*
|
|
22
|
+
* Takendown repos are excluded: re-backfilling one ends in
|
|
23
|
+
* `setRepoStatus(did, 'active')`, which would silently reinstate an account an
|
|
24
|
+
* admin took down — takedowns must survive every redeploy. Reinstatement goes
|
|
25
|
+
* through `/admin/reverse-takedown` only. Exported for tests.
|
|
26
|
+
*/
|
|
27
|
+
export function backfillEligible(status) {
|
|
28
|
+
return status !== 'active' && status !== 'takendown';
|
|
29
|
+
}
|
|
5
30
|
import { emit, timer } from "./logger.js";
|
|
6
|
-
|
|
7
|
-
|
|
31
|
+
import { validateRecord } from '@bigmoves/lexicon';
|
|
32
|
+
import { getLexiconArray } from "./database/schema.js";
|
|
8
33
|
let plcUrl;
|
|
34
|
+
/** Point DID resolution at a PLC directory. `runBackfill` does this too; call it
|
|
35
|
+
* at boot so resolvers that run before the first backfill have somewhere to go. */
|
|
36
|
+
export function configurePlc(url) {
|
|
37
|
+
plcUrl = url;
|
|
38
|
+
}
|
|
39
|
+
const pdsCache = new Map();
|
|
40
|
+
/** A repo's PDS endpoint, cached for the process. For serving blobs, where a
|
|
41
|
+
* stale answer after a migration costs one broken image, not a wrong row. */
|
|
42
|
+
export async function pdsFor(did) {
|
|
43
|
+
const cached = pdsCache.get(did);
|
|
44
|
+
if (cached)
|
|
45
|
+
return cached;
|
|
46
|
+
const { pds } = await resolvePds(did);
|
|
47
|
+
pdsCache.set(did, pds);
|
|
48
|
+
return pds;
|
|
49
|
+
}
|
|
9
50
|
/**
|
|
10
51
|
* Resolves a DID to its PDS endpoint and handle by fetching the DID document.
|
|
11
52
|
*
|
|
12
53
|
* Supports both `did:web` (fetches `/.well-known/did.json`) and `did:plc`
|
|
13
|
-
* (fetches from the PLC directory).
|
|
54
|
+
* (fetches from the PLC directory). Always fetches fresh — DID docs change
|
|
55
|
+
* (handle renames, PDS migrations) and a stale cache silently rewrites stale
|
|
56
|
+
* handles back into `_repos` on every backfill.
|
|
14
57
|
*
|
|
15
58
|
* @example
|
|
16
59
|
* ```ts
|
|
@@ -20,9 +63,6 @@ let plcUrl;
|
|
|
20
63
|
* ```
|
|
21
64
|
*/
|
|
22
65
|
async function resolvePds(did) {
|
|
23
|
-
const cached = pdsCache.get(did);
|
|
24
|
-
if (cached)
|
|
25
|
-
return cached;
|
|
26
66
|
let didDoc;
|
|
27
67
|
if (did.startsWith('did:web:')) {
|
|
28
68
|
const domain = did.slice('did:web:'.length);
|
|
@@ -40,12 +80,10 @@ async function resolvePds(did) {
|
|
|
40
80
|
const pds = didDoc.service?.find((s) => s.id === '#atproto_pds')?.serviceEndpoint;
|
|
41
81
|
if (!pds)
|
|
42
82
|
throw new Error(`No PDS endpoint in DID document for ${did}`);
|
|
43
|
-
//
|
|
83
|
+
// First at:// entry in alsoKnownAs is the canonical handle (per @atproto/identity convention)
|
|
44
84
|
const aka = didDoc.alsoKnownAs?.find((u) => u.startsWith('at://'));
|
|
45
85
|
const handle = aka ? aka.slice('at://'.length) : null;
|
|
46
|
-
|
|
47
|
-
pdsCache.set(did, result);
|
|
48
|
-
return result;
|
|
86
|
+
return { pds, handle };
|
|
49
87
|
}
|
|
50
88
|
/**
|
|
51
89
|
* Paginates through all active repos on a relay/PDS using `com.atproto.sync.listRepos`.
|
|
@@ -121,6 +159,13 @@ async function* listReposByCollection(pdsUrl, collection) {
|
|
|
121
159
|
* ```
|
|
122
160
|
*/
|
|
123
161
|
export async function backfillRepo(did, collections, fetchTimeout) {
|
|
162
|
+
// A takendown repo must never be re-imported: the success path ends in
|
|
163
|
+
// setRepoStatus('active'), which would reinstate the account. Guarding here
|
|
164
|
+
// covers every caller (boot scan, auto-backfill, admin re-index).
|
|
165
|
+
if ((await getRepoStatus(did)) === 'takendown') {
|
|
166
|
+
emit('backfill', 'takedown_skip', { did });
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
124
169
|
const elapsed = timer();
|
|
125
170
|
let count = 0;
|
|
126
171
|
let carSizeBytes;
|
|
@@ -128,6 +173,7 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
128
173
|
let error;
|
|
129
174
|
let resolvedPds;
|
|
130
175
|
let resolvedHandle = null;
|
|
176
|
+
let resolvedSince = null;
|
|
131
177
|
let retryCount;
|
|
132
178
|
let retryAfter;
|
|
133
179
|
const controller = new AbortController();
|
|
@@ -137,45 +183,75 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
137
183
|
resolvedPds = pdsUrl;
|
|
138
184
|
resolvedHandle = handle;
|
|
139
185
|
timeout = setTimeout(() => controller.abort(), fetchTimeout * 1000);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
186
|
+
let lastRev = await getRepoRev(did);
|
|
187
|
+
const baseUrl = `${resolvedPds}/xrpc/com.atproto.sync.getRepo?did=${encodeURIComponent(did)}`;
|
|
188
|
+
let repoUrl = lastRev ? `${baseUrl}&since=${encodeURIComponent(lastRev)}` : baseUrl;
|
|
189
|
+
let res = await fetch(repoUrl, { signal: controller.signal });
|
|
190
|
+
// If the PDS rejected our `since` rev (compacted history), fall back to full import
|
|
191
|
+
if (res.status === 400 && lastRev) {
|
|
192
|
+
lastRev = null;
|
|
193
|
+
res = await fetch(baseUrl, { signal: controller.signal });
|
|
194
|
+
}
|
|
143
195
|
if (!res.ok) {
|
|
144
196
|
const httpErr = new Error(`getRepo failed for ${did}: ${res.status}`);
|
|
145
197
|
httpErr.httpStatus = res.status;
|
|
146
198
|
throw httpErr;
|
|
147
199
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Decode commit to get MST root
|
|
152
|
-
|
|
200
|
+
resolvedSince = lastRev;
|
|
201
|
+
let { roots, blocks, byteLength } = await parseCarStream(res.body);
|
|
202
|
+
carSizeBytes = byteLength;
|
|
203
|
+
// Decode commit to get MST root — if the diff CAR is missing the root block,
|
|
204
|
+
// fall back to a full import (the PDS compacted past our `since` rev)
|
|
205
|
+
let rootData = blocks.get(roots[0]);
|
|
206
|
+
if (!rootData && lastRev) {
|
|
207
|
+
lastRev = null;
|
|
208
|
+
resolvedSince = null;
|
|
209
|
+
res = await fetch(baseUrl, { signal: controller.signal });
|
|
210
|
+
if (!res.ok) {
|
|
211
|
+
const httpErr = new Error(`getRepo failed for ${did}: ${res.status}`);
|
|
212
|
+
httpErr.httpStatus = res.status;
|
|
213
|
+
throw httpErr;
|
|
214
|
+
}
|
|
215
|
+
;
|
|
216
|
+
({ roots, blocks, byteLength } = await parseCarStream(res.body));
|
|
217
|
+
carSizeBytes = byteLength;
|
|
218
|
+
rootData = blocks.get(roots[0]);
|
|
219
|
+
}
|
|
153
220
|
if (!rootData)
|
|
154
221
|
throw new Error(`No root block for ${did}`);
|
|
155
222
|
const { value: commit } = cborDecode(rootData);
|
|
156
223
|
// Walk MST to find all record paths
|
|
157
224
|
const entries = walkMst(blocks, commit.data.$link);
|
|
158
225
|
// Delete existing records for this DID before re-importing so deletions are reflected
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
226
|
+
// Only on full imports (no since) — diff CARs only contain changes.
|
|
227
|
+
//
|
|
228
|
+
// Only the rows this repo holds. A record the same account wrote into a
|
|
229
|
+
// permissioned space is not in its repo: it lives with the space, is read
|
|
230
|
+
// through the space sync, and stays put across a re-import of the repo.
|
|
231
|
+
if (!lastRev) {
|
|
232
|
+
for (const col of purgeableCollections(collections)) {
|
|
233
|
+
const schema = getSchema(col);
|
|
234
|
+
if (!schema)
|
|
235
|
+
continue;
|
|
236
|
+
const ownRows = `SELECT uri FROM ${schema.tableName} WHERE did = $1 AND space IS NULL`;
|
|
237
|
+
for (const child of schema.children) {
|
|
238
|
+
await runSQL(`DELETE FROM ${child.tableName} WHERE parent_uri IN (${ownRows})`, [did]);
|
|
239
|
+
}
|
|
240
|
+
for (const union of schema.unions) {
|
|
241
|
+
for (const branch of union.branches) {
|
|
242
|
+
await runSQL(`DELETE FROM ${branch.tableName} WHERE parent_uri IN (${ownRows})`, [did]);
|
|
243
|
+
}
|
|
170
244
|
}
|
|
245
|
+
await runSQL(`DELETE FROM ${schema.tableName} WHERE did = $1 AND space IS NULL`, [did]);
|
|
171
246
|
}
|
|
172
247
|
}
|
|
173
248
|
// Insert records in chunks to limit memory usage
|
|
174
249
|
const CHUNK_SIZE = 1000;
|
|
175
250
|
let chunk = [];
|
|
251
|
+
const validationSkips = {};
|
|
176
252
|
for (const entry of entries) {
|
|
177
253
|
const collection = entry.path.split('/')[0];
|
|
178
|
-
if (!collections.has(collection))
|
|
254
|
+
if (!collections.has(collection) || isPrivateCollection(collection))
|
|
179
255
|
continue;
|
|
180
256
|
const blockData = blocks.get(entry.cid);
|
|
181
257
|
if (!blockData)
|
|
@@ -187,6 +263,12 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
187
263
|
continue;
|
|
188
264
|
const rkey = entry.path.split('/').slice(1).join('/');
|
|
189
265
|
const uri = `at://${did}/${collection}/${rkey}`;
|
|
266
|
+
const validationError = validateRecord(getLexiconArray(), collection, record);
|
|
267
|
+
if (validationError) {
|
|
268
|
+
validationSkips[collection] = (validationSkips[collection] || 0) + 1;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
noteReferencedRepos(collection, rkey, record);
|
|
190
272
|
chunk.push({ collection, uri, cid: entry.cid, did, record });
|
|
191
273
|
if (chunk.length >= CHUNK_SIZE) {
|
|
192
274
|
count += await bulkInsertRecords(chunk);
|
|
@@ -202,11 +284,13 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
202
284
|
});
|
|
203
285
|
}
|
|
204
286
|
}
|
|
205
|
-
blocks.free();
|
|
206
|
-
blocks = null;
|
|
207
287
|
if (chunk.length > 0) {
|
|
208
288
|
count += await bulkInsertRecords(chunk);
|
|
209
289
|
}
|
|
290
|
+
const totalSkips = Object.values(validationSkips).reduce((a, b) => a + b, 0);
|
|
291
|
+
if (totalSkips > 0) {
|
|
292
|
+
emit('backfill', 'validation_skips', { did, total: totalSkips, by_collection: validationSkips });
|
|
293
|
+
}
|
|
210
294
|
await setRepoStatus(did, 'active', commit.rev, { handle });
|
|
211
295
|
return count;
|
|
212
296
|
}
|
|
@@ -238,6 +322,8 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
238
322
|
error,
|
|
239
323
|
pds_url: resolvedPds,
|
|
240
324
|
car_size_bytes: carSizeBytes,
|
|
325
|
+
import_mode: carSizeBytes !== undefined ? (resolvedSince ? 'diff' : 'full') : undefined,
|
|
326
|
+
since_rev: resolvedSince,
|
|
241
327
|
retry_count: retryCount,
|
|
242
328
|
retry_after: retryAfter,
|
|
243
329
|
permanent_failure: retryCount === 999 ? true : undefined,
|
|
@@ -313,26 +399,30 @@ export async function runBackfill(opts) {
|
|
|
313
399
|
}
|
|
314
400
|
}
|
|
315
401
|
else if (config.fullNetwork) {
|
|
316
|
-
for
|
|
317
|
-
|
|
402
|
+
for (const url of [pdsUrl, ...(opts.extraPdsUrls ?? [])]) {
|
|
403
|
+
for await (const repo of listRepos(url)) {
|
|
404
|
+
dids.add(repo.did);
|
|
405
|
+
}
|
|
318
406
|
}
|
|
319
407
|
}
|
|
320
408
|
else {
|
|
321
|
-
for (const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
catch (err) {
|
|
328
|
-
// Fall back to listRepos if listReposByCollection not supported
|
|
329
|
-
if (err.message.includes('400') || err.message.includes('401') || err.message.includes('501')) {
|
|
330
|
-
for await (const repo of listRepos(pdsUrl)) {
|
|
409
|
+
for (const url of [pdsUrl, ...(opts.extraPdsUrls ?? [])]) {
|
|
410
|
+
for (const col of signalCollections) {
|
|
411
|
+
try {
|
|
412
|
+
for await (const repo of listReposByCollection(url, col)) {
|
|
331
413
|
dids.add(repo.did);
|
|
332
414
|
}
|
|
333
|
-
break;
|
|
334
415
|
}
|
|
335
|
-
|
|
416
|
+
catch (err) {
|
|
417
|
+
// Fall back to listRepos if listReposByCollection not supported
|
|
418
|
+
if (err.message.includes('400') || err.message.includes('401') || err.message.includes('501')) {
|
|
419
|
+
for await (const repo of listRepos(url)) {
|
|
420
|
+
dids.add(repo.did);
|
|
421
|
+
}
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
336
426
|
}
|
|
337
427
|
}
|
|
338
428
|
}
|
|
@@ -340,7 +430,7 @@ export async function runBackfill(opts) {
|
|
|
340
430
|
const pending = [];
|
|
341
431
|
for (const did of dids) {
|
|
342
432
|
const status = await getRepoStatus(did);
|
|
343
|
-
if (status
|
|
433
|
+
if (backfillEligible(status)) {
|
|
344
434
|
if (!status)
|
|
345
435
|
await setRepoStatus(did, 'pending');
|
|
346
436
|
pending.push(did);
|
|
@@ -363,7 +453,7 @@ export async function runBackfill(opts) {
|
|
|
363
453
|
parallelism: config.parallelism,
|
|
364
454
|
status: 'success',
|
|
365
455
|
});
|
|
366
|
-
return;
|
|
456
|
+
return 0;
|
|
367
457
|
}
|
|
368
458
|
// 3. Backfill with worker pool
|
|
369
459
|
let totalRecords = 0;
|
|
@@ -387,7 +477,7 @@ export async function runBackfill(opts) {
|
|
|
387
477
|
retryRound++;
|
|
388
478
|
// Wait until the earliest retry_after has passed
|
|
389
479
|
const now = Math.floor(Date.now() / 1000);
|
|
390
|
-
const rows = await querySQL(`SELECT MIN(retry_after) as earliest FROM _repos WHERE status = 'failed' AND retry_after > $1 AND retry_count < $2`, [now, maxRetries]);
|
|
480
|
+
const rows = (await querySQL(`SELECT MIN(retry_after) as earliest FROM _repos WHERE status = 'failed' AND retry_after > $1 AND retry_count < $2`, [now, maxRetries]));
|
|
391
481
|
const earliest = rows[0]?.earliest ? Number(rows[0].earliest) : 0;
|
|
392
482
|
if (earliest > now) {
|
|
393
483
|
await new Promise((resolve) => setTimeout(resolve, (earliest - now) * 1000));
|
|
@@ -421,4 +511,5 @@ export async function runBackfill(opts) {
|
|
|
421
511
|
retry_rounds: retryRound,
|
|
422
512
|
status: failedCount > 0 ? 'partial' : 'success',
|
|
423
513
|
});
|
|
514
|
+
return totalRecords;
|
|
424
515
|
}
|
package/dist/car.d.ts
CHANGED
|
@@ -27,6 +27,26 @@ export declare class LazyBlockMap {
|
|
|
27
27
|
/** Release the underlying CAR buffer */
|
|
28
28
|
free(): void;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Parses a CARv1 stream incrementally from a `ReadableStream`.
|
|
32
|
+
*
|
|
33
|
+
* Instead of buffering the entire CAR into a single ArrayBuffer, this reads
|
|
34
|
+
* chunks from the stream and parses blocks as they arrive. Each block's data
|
|
35
|
+
* is `.slice()`d into its own small `Uint8Array`, allowing V8 to GC individual
|
|
36
|
+
* blocks as they're consumed during the MST walk.
|
|
37
|
+
*
|
|
38
|
+
* This is critical for backfill where multiple workers download 30-90MB CARs
|
|
39
|
+
* concurrently — buffered downloads cause OOMs because `ArrayBuffer` memory
|
|
40
|
+
* is "external" to V8's heap and not controlled by `--max-old-space-size`.
|
|
41
|
+
*
|
|
42
|
+
* @param body - The response body stream (e.g. `res.body` from `fetch()`)
|
|
43
|
+
* @returns `roots` — root CID strings; `blocks` — map of CID → block data; `byteLength` — total bytes read
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseCarStream(body: ReadableStream<Uint8Array>): Promise<{
|
|
46
|
+
roots: string[];
|
|
47
|
+
blocks: Map<string, Uint8Array>;
|
|
48
|
+
byteLength: number;
|
|
49
|
+
}>;
|
|
30
50
|
/**
|
|
31
51
|
* Parses a CARv1 binary frame into its root CIDs and a lazy block map.
|
|
32
52
|
*
|
package/dist/car.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"car.d.ts","sourceRoot":"","sources":["../src/car.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAuCH;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,QAAQ,CAAmB;gBAEvB,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAKxE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAMxC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI5B,IAAI,IAAI,IAAI,MAAM,CAEjB;IAEA,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAO5D,wCAAwC;IACxC,IAAI,IAAI,IAAI;CAIb;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,UAAU,GAAG;IACnD,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE,YAAY,CAAA;CACrB,CAiCA"}
|
|
1
|
+
{"version":3,"file":"car.d.ts","sourceRoot":"","sources":["../src/car.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAuCH;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,QAAQ,CAAmB;gBAEvB,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAKxE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAMxC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI5B,IAAI,IAAI,IAAI,MAAM,CAEjB;IAEA,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAO5D,wCAAwC;IACxC,IAAI,IAAI,IAAI;CAIb;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC;IAC9E,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IAC/B,UAAU,EAAE,MAAM,CAAA;CACnB,CAAC,CAsGD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,UAAU,GAAG;IACnD,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE,YAAY,CAAA;CACrB,CAiCA"}
|
package/dist/car.js
CHANGED
|
@@ -78,6 +78,114 @@ export class LazyBlockMap {
|
|
|
78
78
|
this.offsets.clear();
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Parses a CARv1 stream incrementally from a `ReadableStream`.
|
|
83
|
+
*
|
|
84
|
+
* Instead of buffering the entire CAR into a single ArrayBuffer, this reads
|
|
85
|
+
* chunks from the stream and parses blocks as they arrive. Each block's data
|
|
86
|
+
* is `.slice()`d into its own small `Uint8Array`, allowing V8 to GC individual
|
|
87
|
+
* blocks as they're consumed during the MST walk.
|
|
88
|
+
*
|
|
89
|
+
* This is critical for backfill where multiple workers download 30-90MB CARs
|
|
90
|
+
* concurrently — buffered downloads cause OOMs because `ArrayBuffer` memory
|
|
91
|
+
* is "external" to V8's heap and not controlled by `--max-old-space-size`.
|
|
92
|
+
*
|
|
93
|
+
* @param body - The response body stream (e.g. `res.body` from `fetch()`)
|
|
94
|
+
* @returns `roots` — root CID strings; `blocks` — map of CID → block data; `byteLength` — total bytes read
|
|
95
|
+
*/
|
|
96
|
+
export async function parseCarStream(body) {
|
|
97
|
+
const reader = body.getReader();
|
|
98
|
+
// Growable buffer with position tracking. We reuse a single allocation and
|
|
99
|
+
// compact (shift data to front) when the read position passes the midpoint,
|
|
100
|
+
// avoiding per-chunk allocations and subarray references that pin old memory.
|
|
101
|
+
let buf = new Uint8Array(64 * 1024);
|
|
102
|
+
let pos = 0; // read cursor
|
|
103
|
+
let len = 0; // bytes of valid data in buf
|
|
104
|
+
let byteLength = 0;
|
|
105
|
+
// Ensure at least `need` bytes are available at buf[pos..pos+need)
|
|
106
|
+
async function fill(need) {
|
|
107
|
+
while (len - pos < need) {
|
|
108
|
+
const { done, value } = await reader.read();
|
|
109
|
+
if (done)
|
|
110
|
+
return len - pos >= need;
|
|
111
|
+
byteLength += value.length;
|
|
112
|
+
// Compact: shift remaining data to front when read cursor passes midpoint
|
|
113
|
+
if (pos > 0 && pos > buf.length >>> 1) {
|
|
114
|
+
buf.copyWithin(0, pos, len);
|
|
115
|
+
len -= pos;
|
|
116
|
+
pos = 0;
|
|
117
|
+
}
|
|
118
|
+
// Grow if needed
|
|
119
|
+
const required = len + value.length;
|
|
120
|
+
if (required > buf.length) {
|
|
121
|
+
const newBuf = new Uint8Array(Math.max(required, buf.length * 2));
|
|
122
|
+
newBuf.set(buf.subarray(0, len));
|
|
123
|
+
buf = newBuf;
|
|
124
|
+
}
|
|
125
|
+
buf.set(value, len);
|
|
126
|
+
len += value.length;
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
function consume(n) {
|
|
131
|
+
pos += n;
|
|
132
|
+
}
|
|
133
|
+
// Read a varint starting at buf[pos]
|
|
134
|
+
function readVarintFromBuf() {
|
|
135
|
+
let value = 0;
|
|
136
|
+
let shift = 0;
|
|
137
|
+
let p = pos;
|
|
138
|
+
while (p < len) {
|
|
139
|
+
const byte = buf[p++];
|
|
140
|
+
value |= (byte & 0x7f) << shift;
|
|
141
|
+
if ((byte & 0x80) === 0)
|
|
142
|
+
return [value, p - pos];
|
|
143
|
+
shift += 7;
|
|
144
|
+
if (shift > 35)
|
|
145
|
+
throw new Error('Varint too long');
|
|
146
|
+
}
|
|
147
|
+
throw new Error('Unexpected end of varint');
|
|
148
|
+
}
|
|
149
|
+
// Parse header: varint(headerLen) + CBOR(header)
|
|
150
|
+
if (!(await fill(1)))
|
|
151
|
+
throw new Error('Empty CAR stream');
|
|
152
|
+
// Prefetch up to 10 bytes for the varint; readVarintFromBuf bounds to `len`
|
|
153
|
+
await fill(10);
|
|
154
|
+
const [headerLen, headerVarintSize] = readVarintFromBuf();
|
|
155
|
+
consume(headerVarintSize);
|
|
156
|
+
if (!(await fill(headerLen)))
|
|
157
|
+
throw new Error('Truncated CAR header');
|
|
158
|
+
// .slice() copies out of the reusable buffer
|
|
159
|
+
const headerSlice = buf.slice(pos, pos + headerLen);
|
|
160
|
+
const { value: header } = cborDecode(headerSlice);
|
|
161
|
+
consume(headerLen);
|
|
162
|
+
const roots = (header.roots || []).map((root) => root?.$link ?? cidToString(root));
|
|
163
|
+
// Parse blocks
|
|
164
|
+
const blocks = new Map();
|
|
165
|
+
while (true) {
|
|
166
|
+
if (!(await fill(1)))
|
|
167
|
+
break;
|
|
168
|
+
// Prefetch up to 10 bytes for the varint; readVarintFromBuf bounds to `len`
|
|
169
|
+
await fill(10);
|
|
170
|
+
const [blockLen, blockVarintSize] = readVarintFromBuf();
|
|
171
|
+
consume(blockVarintSize);
|
|
172
|
+
if (blockLen === 0)
|
|
173
|
+
break;
|
|
174
|
+
if (!(await fill(blockLen)))
|
|
175
|
+
throw new Error('Truncated CAR block');
|
|
176
|
+
const [cidBytes, afterCid] = parseCidFromBytes(buf, pos);
|
|
177
|
+
const cid = cidToString(cidBytes);
|
|
178
|
+
const cidLen = afterCid - pos;
|
|
179
|
+
// .slice() creates an independent copy — the buffer can be reused
|
|
180
|
+
const data = buf.slice(afterCid, afterCid + blockLen - cidLen);
|
|
181
|
+
blocks.set(cid, data);
|
|
182
|
+
consume(blockLen);
|
|
183
|
+
}
|
|
184
|
+
reader.releaseLock();
|
|
185
|
+
// Release the internal buffer
|
|
186
|
+
buf = null;
|
|
187
|
+
return { roots, blocks, byteLength };
|
|
188
|
+
}
|
|
81
189
|
/**
|
|
82
190
|
* Parses a CARv1 binary frame into its root CIDs and a lazy block map.
|
|
83
191
|
*
|