@hatk/hatk 0.0.1-alpha.7 → 0.0.1-alpha.70
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 +108 -0
- package/dist/backfill.d.ts +21 -2
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +116 -43
- package/dist/car.d.ts +42 -10
- package/dist/car.d.ts.map +1 -1
- package/dist/car.js +154 -14
- package/dist/cli.js +243 -1043
- package/dist/config.d.ts +32 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -9
- 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 +23 -0
- package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
- package/dist/database/adapters/sqlite-search.js +74 -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} +57 -6
- package/dist/database/db.d.ts.map +1 -0
- package/dist/{db.js → database/db.js} +755 -552
- 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} +116 -32
- 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} +14 -3
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/{schema.js → database/schema.js} +81 -41
- package/dist/dev-entry.d.ts +8 -0
- package/dist/dev-entry.d.ts.map +1 -0
- package/dist/dev-entry.js +119 -0
- package/dist/feeds.d.ts +12 -8
- package/dist/feeds.d.ts.map +1 -1
- package/dist/feeds.js +51 -6
- package/dist/hooks.d.ts +85 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +163 -0
- package/dist/hydrate.d.ts +7 -6
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +4 -16
- package/dist/indexer.d.ts +43 -0
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +246 -38
- package/dist/labels.d.ts +36 -0
- package/dist/labels.d.ts.map +1 -1
- package/dist/labels.js +71 -6
- 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 +148 -68
- package/dist/mst.d.ts +18 -1
- package/dist/mst.d.ts.map +1 -1
- package/dist/mst.js +19 -8
- package/dist/oauth/db.d.ts +3 -1
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +56 -19
- package/dist/oauth/discovery.d.ts.map +1 -1
- package/dist/oauth/discovery.js +13 -1
- package/dist/oauth/server.d.ts +38 -0
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +228 -31
- package/dist/oauth/session.d.ts +11 -0
- package/dist/oauth/session.d.ts.map +1 -0
- package/dist/oauth/session.js +65 -0
- package/dist/opengraph.d.ts +10 -0
- package/dist/opengraph.d.ts.map +1 -1
- package/dist/opengraph.js +80 -40
- package/dist/pds-proxy.d.ts +60 -0
- package/dist/pds-proxy.d.ts.map +1 -0
- package/dist/pds-proxy.js +277 -0
- package/dist/private-collections.d.ts +8 -0
- package/dist/private-collections.d.ts.map +1 -0
- package/dist/private-collections.js +22 -0
- package/dist/push.d.ts +34 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +186 -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 +26 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +729 -638
- package/dist/setup.d.ts +28 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +50 -3
- package/dist/templates/feed.tpl +14 -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 +41 -0
- package/dist/test.d.ts +1 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +41 -32
- 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 +75 -11
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +189 -39
- package/package.json +17 -9
- package/public/admin-auth.js +271 -306
- package/public/admin.html +177 -61
- package/dist/db.d.ts.map +0 -1
- 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,UAcvB,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,108 @@
|
|
|
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
|
+
'/oauth/',
|
|
62
|
+
'/oauth-client-metadata.json',
|
|
63
|
+
'/.well-known/oauth-authorization-server',
|
|
64
|
+
'/.well-known/oauth-protected-resource',
|
|
65
|
+
'/og/',
|
|
66
|
+
'/admin',
|
|
67
|
+
'/repos',
|
|
68
|
+
'/info/',
|
|
69
|
+
'/_health',
|
|
70
|
+
'/robots.txt',
|
|
71
|
+
'/auth/logout',
|
|
72
|
+
'/__dev/',
|
|
73
|
+
];
|
|
74
|
+
export function isHatkRoute(pathname) {
|
|
75
|
+
return HATK_ROUTES.some((r) => pathname.startsWith(r) || pathname === r);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create a Node.js HTTP server from a Web Standard fetch handler.
|
|
79
|
+
* If a fallback Node middleware is provided, non-hatk routes are sent to it
|
|
80
|
+
* (e.g. SvelteKit's handler from build/handler.js).
|
|
81
|
+
*/
|
|
82
|
+
export function serve(handler, port, base, fallback) {
|
|
83
|
+
const origin = base || `http://localhost:${port}`;
|
|
84
|
+
const server = createServer(async (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(req.url, origin);
|
|
87
|
+
// If we have a fallback (e.g. SvelteKit) and this isn't a hatk route, skip hatk
|
|
88
|
+
if (fallback && !isHatkRoute(url.pathname)) {
|
|
89
|
+
fallback(req, res, () => {
|
|
90
|
+
res.writeHead(404);
|
|
91
|
+
res.end('Not found');
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const request = toRequest(req, origin);
|
|
96
|
+
const response = await handler(request);
|
|
97
|
+
await sendResponse(res, response);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (!res.headersSent) {
|
|
101
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
102
|
+
}
|
|
103
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
server.listen(port);
|
|
107
|
+
return server;
|
|
108
|
+
}
|
package/dist/backfill.d.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
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 {
|
|
@@ -7,7 +26,7 @@ interface BackfillOpts {
|
|
|
7
26
|
plcUrl: string;
|
|
8
27
|
/** AT Protocol collection NSIDs to index (e.g. `app.bsky.feed.post`). */
|
|
9
28
|
collections: Set<string>;
|
|
10
|
-
/** Backfill behavior settings from `config.
|
|
29
|
+
/** Backfill behavior settings from `hatk.config.ts`. */
|
|
11
30
|
config: BackfillConfig;
|
|
12
31
|
}
|
|
13
32
|
/**
|
|
@@ -65,6 +84,6 @@ export declare function backfillRepo(did: string, collections: Set<string>, fetc
|
|
|
65
84
|
* })
|
|
66
85
|
* ```
|
|
67
86
|
*/
|
|
68
|
-
export declare function runBackfill(opts: BackfillOpts): Promise<
|
|
87
|
+
export declare function runBackfill(opts: BackfillOpts): Promise<number>;
|
|
69
88
|
export {};
|
|
70
89
|
//# 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":"AAiBA;;;;;;;;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,8FAA8F;IAC9F,MAAM,EAAE,MAAM,CAAA;IACd,yEAAyE;IACzE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,wDAAwD;IACxD,MAAM,EAAE,cAAc,CAAA;CACvB;AA+FD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAyK/G;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAkIrE"}
|
package/dist/backfill.js
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseCarStream } from "./car.js";
|
|
2
2
|
import { cborDecode } from "./cbor.js";
|
|
3
3
|
import { walkMst } from "./mst.js";
|
|
4
|
-
import { setRepoStatus, getRepoStatus, getRepoRetryInfo, listRetryEligibleRepos, listPendingRepos, querySQL, runSQL, getSchema, bulkInsertRecords, } from "./db.js";
|
|
4
|
+
import { setRepoStatus, getRepoStatus, getRepoRev, getRepoRetryInfo, listRetryEligibleRepos, listPendingRepos, querySQL, runSQL, getSchema, bulkInsertRecords, } from "./database/db.js";
|
|
5
|
+
import { isPrivateCollection } from "./private-collections.js";
|
|
6
|
+
/**
|
|
7
|
+
* The collections whose local rows a full re-import may purge before
|
|
8
|
+
* re-inserting from the repo CAR.
|
|
9
|
+
*
|
|
10
|
+
* Private collections are AppView-authoritative and never present in a repo:
|
|
11
|
+
* purging their rows would destroy data the re-import can never restore (this
|
|
12
|
+
* happened — a login-triggered backfill wiped a deployment's activity rows).
|
|
13
|
+
* Exported for tests.
|
|
14
|
+
*/
|
|
15
|
+
export function purgeableCollections(collections) {
|
|
16
|
+
return [...collections].filter((c) => !isPrivateCollection(c));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether a repo with this stored status should be queued for backfill.
|
|
20
|
+
*
|
|
21
|
+
* Takendown repos are excluded: re-backfilling one ends in
|
|
22
|
+
* `setRepoStatus(did, 'active')`, which would silently reinstate an account an
|
|
23
|
+
* admin took down — takedowns must survive every redeploy. Reinstatement goes
|
|
24
|
+
* through `/admin/reverse-takedown` only. Exported for tests.
|
|
25
|
+
*/
|
|
26
|
+
export function backfillEligible(status) {
|
|
27
|
+
return status !== 'active' && status !== 'takendown';
|
|
28
|
+
}
|
|
5
29
|
import { emit, timer } from "./logger.js";
|
|
6
|
-
|
|
7
|
-
|
|
30
|
+
import { validateRecord } from '@bigmoves/lexicon';
|
|
31
|
+
import { getLexiconArray } from "./database/schema.js";
|
|
8
32
|
let plcUrl;
|
|
9
33
|
/**
|
|
10
34
|
* Resolves a DID to its PDS endpoint and handle by fetching the DID document.
|
|
11
35
|
*
|
|
12
36
|
* Supports both `did:web` (fetches `/.well-known/did.json`) and `did:plc`
|
|
13
|
-
* (fetches from the PLC directory).
|
|
37
|
+
* (fetches from the PLC directory). Always fetches fresh — DID docs change
|
|
38
|
+
* (handle renames, PDS migrations) and a stale cache silently rewrites stale
|
|
39
|
+
* handles back into `_repos` on every backfill.
|
|
14
40
|
*
|
|
15
41
|
* @example
|
|
16
42
|
* ```ts
|
|
@@ -20,9 +46,6 @@ let plcUrl;
|
|
|
20
46
|
* ```
|
|
21
47
|
*/
|
|
22
48
|
async function resolvePds(did) {
|
|
23
|
-
const cached = pdsCache.get(did);
|
|
24
|
-
if (cached)
|
|
25
|
-
return cached;
|
|
26
49
|
let didDoc;
|
|
27
50
|
if (did.startsWith('did:web:')) {
|
|
28
51
|
const domain = did.slice('did:web:'.length);
|
|
@@ -40,12 +63,10 @@ async function resolvePds(did) {
|
|
|
40
63
|
const pds = didDoc.service?.find((s) => s.id === '#atproto_pds')?.serviceEndpoint;
|
|
41
64
|
if (!pds)
|
|
42
65
|
throw new Error(`No PDS endpoint in DID document for ${did}`);
|
|
43
|
-
//
|
|
66
|
+
// First at:// entry in alsoKnownAs is the canonical handle (per @atproto/identity convention)
|
|
44
67
|
const aka = didDoc.alsoKnownAs?.find((u) => u.startsWith('at://'));
|
|
45
68
|
const handle = aka ? aka.slice('at://'.length) : null;
|
|
46
|
-
|
|
47
|
-
pdsCache.set(did, result);
|
|
48
|
-
return result;
|
|
69
|
+
return { pds, handle };
|
|
49
70
|
}
|
|
50
71
|
/**
|
|
51
72
|
* Paginates through all active repos on a relay/PDS using `com.atproto.sync.listRepos`.
|
|
@@ -121,6 +142,13 @@ async function* listReposByCollection(pdsUrl, collection) {
|
|
|
121
142
|
* ```
|
|
122
143
|
*/
|
|
123
144
|
export async function backfillRepo(did, collections, fetchTimeout) {
|
|
145
|
+
// A takendown repo must never be re-imported: the success path ends in
|
|
146
|
+
// setRepoStatus('active'), which would reinstate the account. Guarding here
|
|
147
|
+
// covers every caller (boot scan, auto-backfill, admin re-index).
|
|
148
|
+
if ((await getRepoStatus(did)) === 'takendown') {
|
|
149
|
+
emit('backfill', 'takedown_skip', { did });
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
124
152
|
const elapsed = timer();
|
|
125
153
|
let count = 0;
|
|
126
154
|
let carSizeBytes;
|
|
@@ -128,6 +156,7 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
128
156
|
let error;
|
|
129
157
|
let resolvedPds;
|
|
130
158
|
let resolvedHandle = null;
|
|
159
|
+
let resolvedSince = null;
|
|
131
160
|
let retryCount;
|
|
132
161
|
let retryAfter;
|
|
133
162
|
const controller = new AbortController();
|
|
@@ -137,40 +166,91 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
137
166
|
resolvedPds = pdsUrl;
|
|
138
167
|
resolvedHandle = handle;
|
|
139
168
|
timeout = setTimeout(() => controller.abort(), fetchTimeout * 1000);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
169
|
+
let lastRev = await getRepoRev(did);
|
|
170
|
+
const baseUrl = `${resolvedPds}/xrpc/com.atproto.sync.getRepo?did=${encodeURIComponent(did)}`;
|
|
171
|
+
let repoUrl = lastRev ? `${baseUrl}&since=${encodeURIComponent(lastRev)}` : baseUrl;
|
|
172
|
+
let res = await fetch(repoUrl, { signal: controller.signal });
|
|
173
|
+
// If the PDS rejected our `since` rev (compacted history), fall back to full import
|
|
174
|
+
if (res.status === 400 && lastRev) {
|
|
175
|
+
lastRev = null;
|
|
176
|
+
res = await fetch(baseUrl, { signal: controller.signal });
|
|
177
|
+
}
|
|
143
178
|
if (!res.ok) {
|
|
144
179
|
const httpErr = new Error(`getRepo failed for ${did}: ${res.status}`);
|
|
145
180
|
httpErr.httpStatus = res.status;
|
|
146
181
|
throw httpErr;
|
|
147
182
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
|
|
183
|
+
resolvedSince = lastRev;
|
|
184
|
+
let { roots, blocks, byteLength } = await parseCarStream(res.body);
|
|
185
|
+
carSizeBytes = byteLength;
|
|
186
|
+
// Decode commit to get MST root — if the diff CAR is missing the root block,
|
|
187
|
+
// fall back to a full import (the PDS compacted past our `since` rev)
|
|
188
|
+
let rootData = blocks.get(roots[0]);
|
|
189
|
+
if (!rootData && lastRev) {
|
|
190
|
+
lastRev = null;
|
|
191
|
+
resolvedSince = null;
|
|
192
|
+
res = await fetch(baseUrl, { signal: controller.signal });
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
const httpErr = new Error(`getRepo failed for ${did}: ${res.status}`);
|
|
195
|
+
httpErr.httpStatus = res.status;
|
|
196
|
+
throw httpErr;
|
|
197
|
+
}
|
|
198
|
+
;
|
|
199
|
+
({ roots, blocks, byteLength } = await parseCarStream(res.body));
|
|
200
|
+
carSizeBytes = byteLength;
|
|
201
|
+
rootData = blocks.get(roots[0]);
|
|
202
|
+
}
|
|
154
203
|
if (!rootData)
|
|
155
204
|
throw new Error(`No root block for ${did}`);
|
|
156
205
|
const { value: commit } = cborDecode(rootData);
|
|
157
206
|
// Walk MST to find all record paths
|
|
158
207
|
const entries = walkMst(blocks, commit.data.$link);
|
|
159
|
-
|
|
208
|
+
// Delete existing records for this DID before re-importing so deletions are reflected
|
|
209
|
+
// Only on full imports (no since) — diff CARs only contain changes
|
|
210
|
+
if (!lastRev) {
|
|
211
|
+
for (const col of purgeableCollections(collections)) {
|
|
212
|
+
const schema = getSchema(col);
|
|
213
|
+
if (!schema)
|
|
214
|
+
continue;
|
|
215
|
+
await runSQL(`DELETE FROM ${schema.tableName} WHERE did = $1`, [did]);
|
|
216
|
+
for (const child of schema.children) {
|
|
217
|
+
await runSQL(`DELETE FROM ${child.tableName} WHERE parent_did = $1`, [did]);
|
|
218
|
+
}
|
|
219
|
+
for (const union of schema.unions) {
|
|
220
|
+
for (const branch of union.branches) {
|
|
221
|
+
await runSQL(`DELETE FROM ${branch.tableName} WHERE parent_did = $1`, [did]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Insert records in chunks to limit memory usage
|
|
227
|
+
const CHUNK_SIZE = 1000;
|
|
228
|
+
let chunk = [];
|
|
229
|
+
const validationSkips = {};
|
|
160
230
|
for (const entry of entries) {
|
|
161
231
|
const collection = entry.path.split('/')[0];
|
|
162
|
-
if (!collections.has(collection))
|
|
232
|
+
if (!collections.has(collection) || isPrivateCollection(collection))
|
|
163
233
|
continue;
|
|
164
234
|
const blockData = blocks.get(entry.cid);
|
|
165
235
|
if (!blockData)
|
|
166
236
|
continue;
|
|
237
|
+
blocks.delete(entry.cid); // free block data as we go
|
|
167
238
|
try {
|
|
168
239
|
const { value: record } = cborDecode(blockData);
|
|
169
240
|
if (!record?.$type)
|
|
170
241
|
continue;
|
|
171
242
|
const rkey = entry.path.split('/').slice(1).join('/');
|
|
172
243
|
const uri = `at://${did}/${collection}/${rkey}`;
|
|
173
|
-
|
|
244
|
+
const validationError = validateRecord(getLexiconArray(), collection, record);
|
|
245
|
+
if (validationError) {
|
|
246
|
+
validationSkips[collection] = (validationSkips[collection] || 0) + 1;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
chunk.push({ collection, uri, cid: entry.cid, did, record });
|
|
250
|
+
if (chunk.length >= CHUNK_SIZE) {
|
|
251
|
+
count += await bulkInsertRecords(chunk);
|
|
252
|
+
chunk = [];
|
|
253
|
+
}
|
|
174
254
|
}
|
|
175
255
|
catch (recordErr) {
|
|
176
256
|
emit('backfill', 'record_error', {
|
|
@@ -181,23 +261,13 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
181
261
|
});
|
|
182
262
|
}
|
|
183
263
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
await runSQL(`DELETE FROM ${schema.tableName} WHERE did = $1`, did);
|
|
191
|
-
for (const child of schema.children) {
|
|
192
|
-
await runSQL(`DELETE FROM ${child.tableName} WHERE parent_did = $1`, did);
|
|
193
|
-
}
|
|
194
|
-
for (const union of schema.unions) {
|
|
195
|
-
for (const branch of union.branches) {
|
|
196
|
-
await runSQL(`DELETE FROM ${branch.tableName} WHERE parent_did = $1`, did);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
264
|
+
if (chunk.length > 0) {
|
|
265
|
+
count += await bulkInsertRecords(chunk);
|
|
266
|
+
}
|
|
267
|
+
const totalSkips = Object.values(validationSkips).reduce((a, b) => a + b, 0);
|
|
268
|
+
if (totalSkips > 0) {
|
|
269
|
+
emit('backfill', 'validation_skips', { did, total: totalSkips, by_collection: validationSkips });
|
|
199
270
|
}
|
|
200
|
-
count = await bulkInsertRecords(bulk);
|
|
201
271
|
await setRepoStatus(did, 'active', commit.rev, { handle });
|
|
202
272
|
return count;
|
|
203
273
|
}
|
|
@@ -229,6 +299,8 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
229
299
|
error,
|
|
230
300
|
pds_url: resolvedPds,
|
|
231
301
|
car_size_bytes: carSizeBytes,
|
|
302
|
+
import_mode: carSizeBytes !== undefined ? (resolvedSince ? 'diff' : 'full') : undefined,
|
|
303
|
+
since_rev: resolvedSince,
|
|
232
304
|
retry_count: retryCount,
|
|
233
305
|
retry_after: retryAfter,
|
|
234
306
|
permanent_failure: retryCount === 999 ? true : undefined,
|
|
@@ -331,7 +403,7 @@ export async function runBackfill(opts) {
|
|
|
331
403
|
const pending = [];
|
|
332
404
|
for (const did of dids) {
|
|
333
405
|
const status = await getRepoStatus(did);
|
|
334
|
-
if (status
|
|
406
|
+
if (backfillEligible(status)) {
|
|
335
407
|
if (!status)
|
|
336
408
|
await setRepoStatus(did, 'pending');
|
|
337
409
|
pending.push(did);
|
|
@@ -354,7 +426,7 @@ export async function runBackfill(opts) {
|
|
|
354
426
|
parallelism: config.parallelism,
|
|
355
427
|
status: 'success',
|
|
356
428
|
});
|
|
357
|
-
return;
|
|
429
|
+
return 0;
|
|
358
430
|
}
|
|
359
431
|
// 3. Backfill with worker pool
|
|
360
432
|
let totalRecords = 0;
|
|
@@ -378,7 +450,7 @@ export async function runBackfill(opts) {
|
|
|
378
450
|
retryRound++;
|
|
379
451
|
// Wait until the earliest retry_after has passed
|
|
380
452
|
const now = Math.floor(Date.now() / 1000);
|
|
381
|
-
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]);
|
|
453
|
+
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]));
|
|
382
454
|
const earliest = rows[0]?.earliest ? Number(rows[0].earliest) : 0;
|
|
383
455
|
if (earliest > now) {
|
|
384
456
|
await new Promise((resolve) => setTimeout(resolve, (earliest - now) * 1000));
|
|
@@ -412,4 +484,5 @@ export async function runBackfill(opts) {
|
|
|
412
484
|
retry_rounds: retryRound,
|
|
413
485
|
status: failedCount > 0 ? 'partial' : 'success',
|
|
414
486
|
});
|
|
487
|
+
return totalRecords;
|
|
415
488
|
}
|
package/dist/car.d.ts
CHANGED
|
@@ -12,20 +12,52 @@
|
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* A memory-efficient block map that stores byte offsets into the original CAR
|
|
16
|
+
* buffer instead of copying block data. Implements the same `get`/`delete`/`size`
|
|
17
|
+
* interface as `Map<string, Uint8Array>` so it can be used as a drop-in replacement.
|
|
18
|
+
*/
|
|
19
|
+
export declare class LazyBlockMap {
|
|
20
|
+
private offsets;
|
|
21
|
+
private carBytes;
|
|
22
|
+
constructor(carBytes: Uint8Array, offsets: Map<string, [number, number]>);
|
|
23
|
+
get(cid: string): Uint8Array | undefined;
|
|
24
|
+
delete(cid: string): boolean;
|
|
25
|
+
get size(): number;
|
|
26
|
+
[Symbol.iterator](): IterableIterator<[string, Uint8Array]>;
|
|
27
|
+
/** Release the underlying CAR buffer */
|
|
28
|
+
free(): void;
|
|
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
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Parses a CARv1 binary frame into its root CIDs and a lazy block map.
|
|
52
|
+
*
|
|
53
|
+
* The block map stores byte offsets into `carBytes` rather than copying data,
|
|
54
|
+
* reducing heap usage from O(total block bytes) to O(number of blocks * 16 bytes).
|
|
16
55
|
*
|
|
17
56
|
* @param carBytes - Raw CAR file bytes (e.g. from `getRepo` or a firehose commit)
|
|
18
|
-
* @returns `roots` — ordered list of root CID strings; `blocks` —
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```ts
|
|
22
|
-
* const car = new Uint8Array(await res.arrayBuffer())
|
|
23
|
-
* const { roots, blocks } = parseCarFrame(car)
|
|
24
|
-
* const commitData = blocks.get(roots[0])
|
|
25
|
-
* ```
|
|
57
|
+
* @returns `roots` — ordered list of root CID strings; `blocks` — lazy block map
|
|
26
58
|
*/
|
|
27
59
|
export declare function parseCarFrame(carBytes: Uint8Array): {
|
|
28
60
|
roots: string[];
|
|
29
|
-
blocks:
|
|
61
|
+
blocks: LazyBlockMap;
|
|
30
62
|
};
|
|
31
63
|
//# sourceMappingURL=car.d.ts.map
|
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
|
|
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"}
|