@hatk/hatk 0.0.1-alpha.1 → 0.0.1-alpha.100
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 +90 -1
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +254 -55
- package/dist/car.d.ts +59 -1
- package/dist/car.d.ts.map +1 -1
- package/dist/car.js +179 -7
- package/dist/cbor.d.ts +37 -0
- package/dist/cbor.d.ts.map +1 -1
- package/dist/cbor.js +36 -3
- package/dist/cid.d.ts +37 -0
- package/dist/cid.d.ts.map +1 -1
- package/dist/cid.js +38 -3
- package/dist/cli.js +247 -986
- package/dist/config.d.ts +145 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +62 -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 +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} +94 -6
- package/dist/database/db.d.ts.map +1 -0
- package/dist/database/db.js +1833 -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/database/schema.d.ts +91 -0
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/{schema.js → database/schema.js} +153 -54
- package/dist/dev-entry.d.ts +8 -0
- package/dist/dev-entry.d.ts.map +1 -0
- package/dist/dev-entry.js +126 -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 +55 -6
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +21 -20
- package/dist/indexer.d.ts +195 -3
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +634 -94
- package/dist/jetstream.d.ts +118 -0
- package/dist/jetstream.d.ts.map +1 -0
- package/dist/jetstream.js +238 -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 +197 -75
- 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 +27 -1
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +80 -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 +131 -1
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +504 -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 +92 -0
- package/dist/pds-proxy.d.ts.map +1 -0
- package/dist/pds-proxy.js +396 -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 +1053 -626
- 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 +112 -0
- package/dist/spaces/blob.d.ts.map +1 -0
- package/dist/spaces/blob.js +460 -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 +46 -0
- package/dist/spaces/verify.d.ts.map +1 -0
- package/dist/spaces/verify.js +94 -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 -65
- package/dist/xrpc.d.ts +122 -12
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +270 -55
- package/package.json +34 -10
- package/public/admin-auth.js +271 -306
- package/public/admin.html +177 -61
- package/public/robots.txt +2 -0
- 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 +0 -51
- 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
package/dist/backfill.js
CHANGED
|
@@ -1,14 +1,73 @@
|
|
|
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. Deactivated and deleted repos are
|
|
26
|
+
* excluded for the same reason from the other side: the network said so, and
|
|
27
|
+
* only the network saying otherwise (an `#account` event) brings them back.
|
|
28
|
+
* Exported for tests.
|
|
29
|
+
*/
|
|
30
|
+
export function backfillEligible(status) {
|
|
31
|
+
return !status || !NOT_BACKFILLED.has(status);
|
|
32
|
+
}
|
|
33
|
+
/** Statuses a backfill must leave alone. See {@link backfillEligible}. */
|
|
34
|
+
const NOT_BACKFILLED = new Set(['active', 'takendown', 'deactivated', 'deleted']);
|
|
5
35
|
import { emit, timer } from "./logger.js";
|
|
6
|
-
|
|
36
|
+
import { validateRecord } from '@bigmoves/lexicon';
|
|
37
|
+
import { getLexiconArray } from "./database/schema.js";
|
|
7
38
|
let plcUrl;
|
|
8
|
-
|
|
39
|
+
/** Point DID resolution at a PLC directory. `runBackfill` does this too; call it
|
|
40
|
+
* at boot so resolvers that run before the first backfill have somewhere to go. */
|
|
41
|
+
export function configurePlc(url) {
|
|
42
|
+
plcUrl = url;
|
|
43
|
+
}
|
|
44
|
+
const pdsCache = new Map();
|
|
45
|
+
/** A repo's PDS endpoint, cached for the process. For serving blobs, where a
|
|
46
|
+
* stale answer after a migration costs one broken image, not a wrong row. */
|
|
47
|
+
export async function pdsFor(did) {
|
|
9
48
|
const cached = pdsCache.get(did);
|
|
10
49
|
if (cached)
|
|
11
50
|
return cached;
|
|
51
|
+
const { pds } = await resolvePds(did);
|
|
52
|
+
pdsCache.set(did, pds);
|
|
53
|
+
return pds;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolves a DID to its PDS endpoint and handle by fetching the DID document.
|
|
57
|
+
*
|
|
58
|
+
* Supports both `did:web` (fetches `/.well-known/did.json`) and `did:plc`
|
|
59
|
+
* (fetches from the PLC directory). Always fetches fresh — DID docs change
|
|
60
|
+
* (handle renames, PDS migrations) and a stale cache silently rewrites stale
|
|
61
|
+
* handles back into `_repos` on every backfill.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const { pds, handle } = await resolvePds('did:plc:abc123')
|
|
66
|
+
* // pds = "https://puffball.us-east.host.bsky.network"
|
|
67
|
+
* // handle = "alice.bsky.social"
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
async function resolvePds(did) {
|
|
12
71
|
let didDoc;
|
|
13
72
|
if (did.startsWith('did:web:')) {
|
|
14
73
|
const domain = did.slice('did:web:'.length);
|
|
@@ -26,14 +85,15 @@ async function resolvePds(did) {
|
|
|
26
85
|
const pds = didDoc.service?.find((s) => s.id === '#atproto_pds')?.serviceEndpoint;
|
|
27
86
|
if (!pds)
|
|
28
87
|
throw new Error(`No PDS endpoint in DID document for ${did}`);
|
|
29
|
-
//
|
|
88
|
+
// First at:// entry in alsoKnownAs is the canonical handle (per @atproto/identity convention)
|
|
30
89
|
const aka = didDoc.alsoKnownAs?.find((u) => u.startsWith('at://'));
|
|
31
90
|
const handle = aka ? aka.slice('at://'.length) : null;
|
|
32
|
-
|
|
33
|
-
pdsCache.set(did, result);
|
|
34
|
-
return result;
|
|
91
|
+
return { pds, handle };
|
|
35
92
|
}
|
|
36
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Paginates through all active repos on a relay/PDS using `com.atproto.sync.listRepos`.
|
|
95
|
+
* Yields `{ did, rev }` for each active repo. Skips deactivated repos.
|
|
96
|
+
*/
|
|
37
97
|
async function* listRepos(pdsUrl) {
|
|
38
98
|
let cursor;
|
|
39
99
|
while (true) {
|
|
@@ -53,6 +113,13 @@ async function* listRepos(pdsUrl) {
|
|
|
53
113
|
cursor = data.cursor;
|
|
54
114
|
}
|
|
55
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Paginates through repos that contain records in a specific collection using
|
|
118
|
+
* `com.atproto.sync.listReposByCollection`. More efficient than {@link listRepos}
|
|
119
|
+
* when only a few collections are needed, since the relay can filter server-side.
|
|
120
|
+
*
|
|
121
|
+
* Not all relays support this endpoint — callers should fall back to {@link listRepos}.
|
|
122
|
+
*/
|
|
56
123
|
async function* listReposByCollection(pdsUrl, collection) {
|
|
57
124
|
let cursor;
|
|
58
125
|
while (true) {
|
|
@@ -71,8 +138,46 @@ async function* listReposByCollection(pdsUrl, collection) {
|
|
|
71
138
|
cursor = data.cursor;
|
|
72
139
|
}
|
|
73
140
|
}
|
|
74
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Downloads and indexes a single user's repo via `com.atproto.sync.getRepo`.
|
|
143
|
+
*
|
|
144
|
+
* The full flow:
|
|
145
|
+
* 1. Resolve the DID to find the user's PDS endpoint
|
|
146
|
+
* 2. Fetch the repo as a CAR file from the PDS
|
|
147
|
+
* 3. Parse the CAR, decode the commit, and walk the MST (Merkle Search Tree)
|
|
148
|
+
* 4. Delete any existing records for this DID (so deletions are reflected)
|
|
149
|
+
* 5. Bulk-insert all records matching the target collections
|
|
150
|
+
*
|
|
151
|
+
* On failure, applies exponential backoff retry logic. HTTP 4xx errors are
|
|
152
|
+
* treated as permanent failures (repo doesn't exist or is deactivated) and
|
|
153
|
+
* are not retried.
|
|
154
|
+
*
|
|
155
|
+
* @param did - The DID of the repo to backfill (e.g. `did:plc:abc123`)
|
|
156
|
+
* @param collections - Collection NSIDs to index; records in other collections are skipped
|
|
157
|
+
* @param fetchTimeout - Maximum seconds to wait for the CAR download before aborting
|
|
158
|
+
* @returns The number of records successfully indexed
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* const count = await backfillRepo('did:plc:abc123', new Set(['app.bsky.feed.post']), 30)
|
|
163
|
+
* console.log(`Indexed ${count} records`)
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
75
166
|
export async function backfillRepo(did, collections, fetchTimeout) {
|
|
167
|
+
// A takendown repo must never be re-imported: the success path ends in
|
|
168
|
+
// setRepoStatus('active'), which would reinstate the account. Nor a
|
|
169
|
+
// deactivated or deleted one, which the network has said is not there.
|
|
170
|
+
// Guarding here covers every caller (boot scan, auto-backfill, admin
|
|
171
|
+
// re-index).
|
|
172
|
+
const current = await getRepoStatus(did);
|
|
173
|
+
if (current === 'takendown') {
|
|
174
|
+
emit('backfill', 'takedown_skip', { did });
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
if (current === 'deactivated' || current === 'deleted') {
|
|
178
|
+
emit('backfill', 'inactive_skip', { did, status: current });
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
76
181
|
const elapsed = timer();
|
|
77
182
|
let count = 0;
|
|
78
183
|
let carSizeBytes;
|
|
@@ -80,6 +185,7 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
80
185
|
let error;
|
|
81
186
|
let resolvedPds;
|
|
82
187
|
let resolvedHandle = null;
|
|
188
|
+
let resolvedSince = null;
|
|
83
189
|
let retryCount;
|
|
84
190
|
let retryAfter;
|
|
85
191
|
const controller = new AbortController();
|
|
@@ -89,39 +195,97 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
89
195
|
resolvedPds = pdsUrl;
|
|
90
196
|
resolvedHandle = handle;
|
|
91
197
|
timeout = setTimeout(() => controller.abort(), fetchTimeout * 1000);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
198
|
+
let lastRev = await getRepoRev(did);
|
|
199
|
+
const baseUrl = `${resolvedPds}/xrpc/com.atproto.sync.getRepo?did=${encodeURIComponent(did)}`;
|
|
200
|
+
let repoUrl = lastRev ? `${baseUrl}&since=${encodeURIComponent(lastRev)}` : baseUrl;
|
|
201
|
+
let res = await fetch(repoUrl, { signal: controller.signal });
|
|
202
|
+
// If the PDS rejected our `since` rev (compacted history), fall back to full import
|
|
203
|
+
if (res.status === 400 && lastRev) {
|
|
204
|
+
lastRev = null;
|
|
205
|
+
res = await fetch(baseUrl, { signal: controller.signal });
|
|
206
|
+
}
|
|
95
207
|
if (!res.ok) {
|
|
96
208
|
const httpErr = new Error(`getRepo failed for ${did}: ${res.status}`);
|
|
97
209
|
httpErr.httpStatus = res.status;
|
|
98
210
|
throw httpErr;
|
|
99
211
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// Decode commit to get MST root
|
|
104
|
-
|
|
212
|
+
resolvedSince = lastRev;
|
|
213
|
+
let { roots, blocks, byteLength } = await parseCarStream(res.body);
|
|
214
|
+
carSizeBytes = byteLength;
|
|
215
|
+
// Decode commit to get MST root — if the diff CAR is missing the root block,
|
|
216
|
+
// fall back to a full import (the PDS compacted past our `since` rev)
|
|
217
|
+
let rootData = blocks.get(roots[0]);
|
|
218
|
+
if (!rootData && lastRev) {
|
|
219
|
+
lastRev = null;
|
|
220
|
+
resolvedSince = null;
|
|
221
|
+
res = await fetch(baseUrl, { signal: controller.signal });
|
|
222
|
+
if (!res.ok) {
|
|
223
|
+
const httpErr = new Error(`getRepo failed for ${did}: ${res.status}`);
|
|
224
|
+
httpErr.httpStatus = res.status;
|
|
225
|
+
throw httpErr;
|
|
226
|
+
}
|
|
227
|
+
;
|
|
228
|
+
({ roots, blocks, byteLength } = await parseCarStream(res.body));
|
|
229
|
+
carSizeBytes = byteLength;
|
|
230
|
+
rootData = blocks.get(roots[0]);
|
|
231
|
+
}
|
|
105
232
|
if (!rootData)
|
|
106
233
|
throw new Error(`No root block for ${did}`);
|
|
107
234
|
const { value: commit } = cborDecode(rootData);
|
|
108
235
|
// Walk MST to find all record paths
|
|
109
236
|
const entries = walkMst(blocks, commit.data.$link);
|
|
110
|
-
|
|
237
|
+
// Delete existing records for this DID before re-importing so deletions are reflected
|
|
238
|
+
// Only on full imports (no since) — diff CARs only contain changes.
|
|
239
|
+
//
|
|
240
|
+
// Only the rows this repo holds. A record the same account wrote into a
|
|
241
|
+
// permissioned space is not in its repo: it lives with the space, is read
|
|
242
|
+
// through the space sync, and stays put across a re-import of the repo.
|
|
243
|
+
if (!lastRev) {
|
|
244
|
+
for (const col of purgeableCollections(collections)) {
|
|
245
|
+
const schema = getSchema(col);
|
|
246
|
+
if (!schema)
|
|
247
|
+
continue;
|
|
248
|
+
const ownRows = `SELECT uri FROM ${schema.tableName} WHERE did = $1 AND space IS NULL`;
|
|
249
|
+
for (const child of schema.children) {
|
|
250
|
+
await runSQL(`DELETE FROM ${child.tableName} WHERE parent_uri IN (${ownRows})`, [did]);
|
|
251
|
+
}
|
|
252
|
+
for (const union of schema.unions) {
|
|
253
|
+
for (const branch of union.branches) {
|
|
254
|
+
await runSQL(`DELETE FROM ${branch.tableName} WHERE parent_uri IN (${ownRows})`, [did]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
await runSQL(`DELETE FROM ${schema.tableName} WHERE did = $1 AND space IS NULL`, [did]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Insert records in chunks to limit memory usage
|
|
261
|
+
const CHUNK_SIZE = 1000;
|
|
262
|
+
let chunk = [];
|
|
263
|
+
const validationSkips = {};
|
|
111
264
|
for (const entry of entries) {
|
|
112
265
|
const collection = entry.path.split('/')[0];
|
|
113
|
-
if (!collections.has(collection))
|
|
266
|
+
if (!collections.has(collection) || isPrivateCollection(collection))
|
|
114
267
|
continue;
|
|
115
268
|
const blockData = blocks.get(entry.cid);
|
|
116
269
|
if (!blockData)
|
|
117
270
|
continue;
|
|
271
|
+
blocks.delete(entry.cid); // free block data as we go
|
|
118
272
|
try {
|
|
119
273
|
const { value: record } = cborDecode(blockData);
|
|
120
274
|
if (!record?.$type)
|
|
121
275
|
continue;
|
|
122
276
|
const rkey = entry.path.split('/').slice(1).join('/');
|
|
123
277
|
const uri = `at://${did}/${collection}/${rkey}`;
|
|
124
|
-
|
|
278
|
+
const validationError = validateRecord(getLexiconArray(), collection, record);
|
|
279
|
+
if (validationError) {
|
|
280
|
+
validationSkips[collection] = (validationSkips[collection] || 0) + 1;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
noteReferencedRepos(collection, rkey, record);
|
|
284
|
+
chunk.push({ collection, uri, cid: entry.cid, did, record });
|
|
285
|
+
if (chunk.length >= CHUNK_SIZE) {
|
|
286
|
+
count += await bulkInsertRecords(chunk);
|
|
287
|
+
chunk = [];
|
|
288
|
+
}
|
|
125
289
|
}
|
|
126
290
|
catch (recordErr) {
|
|
127
291
|
emit('backfill', 'record_error', {
|
|
@@ -132,22 +296,13 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
132
296
|
});
|
|
133
297
|
}
|
|
134
298
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
for (const child of schema.children) {
|
|
142
|
-
await runSQL(`DELETE FROM ${child.tableName} WHERE parent_did = $1`, did);
|
|
143
|
-
}
|
|
144
|
-
for (const union of schema.unions) {
|
|
145
|
-
for (const branch of union.branches) {
|
|
146
|
-
await runSQL(`DELETE FROM ${branch.tableName} WHERE parent_did = $1`, did);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
299
|
+
if (chunk.length > 0) {
|
|
300
|
+
count += await bulkInsertRecords(chunk);
|
|
301
|
+
}
|
|
302
|
+
const totalSkips = Object.values(validationSkips).reduce((a, b) => a + b, 0);
|
|
303
|
+
if (totalSkips > 0) {
|
|
304
|
+
emit('backfill', 'validation_skips', { did, total: totalSkips, by_collection: validationSkips });
|
|
149
305
|
}
|
|
150
|
-
count = await bulkInsertRecords(bulk);
|
|
151
306
|
await setRepoStatus(did, 'active', commit.rev, { handle });
|
|
152
307
|
return count;
|
|
153
308
|
}
|
|
@@ -179,13 +334,24 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
179
334
|
error,
|
|
180
335
|
pds_url: resolvedPds,
|
|
181
336
|
car_size_bytes: carSizeBytes,
|
|
337
|
+
import_mode: carSizeBytes !== undefined ? (resolvedSince ? 'diff' : 'full') : undefined,
|
|
338
|
+
since_rev: resolvedSince,
|
|
182
339
|
retry_count: retryCount,
|
|
183
340
|
retry_after: retryAfter,
|
|
184
341
|
permanent_failure: retryCount === 999 ? true : undefined,
|
|
185
342
|
});
|
|
186
343
|
}
|
|
187
344
|
}
|
|
188
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Processes items concurrently with a fixed number of workers.
|
|
347
|
+
* Workers pull from a shared index so the pool stays saturated even when
|
|
348
|
+
* individual items complete at different speeds. Errors from `fn` are
|
|
349
|
+
* swallowed (they're expected to be captured via structured logging).
|
|
350
|
+
*
|
|
351
|
+
* @param items - The work items to process
|
|
352
|
+
* @param parallelism - Maximum number of concurrent workers
|
|
353
|
+
* @param fn - Async function to run for each item
|
|
354
|
+
*/
|
|
189
355
|
async function runWorkerPool(items, parallelism, fn) {
|
|
190
356
|
let index = 0;
|
|
191
357
|
async function worker() {
|
|
@@ -202,7 +368,35 @@ async function runWorkerPool(items, parallelism, fn) {
|
|
|
202
368
|
const workers = Array.from({ length: Math.min(parallelism, items.length) }, () => worker());
|
|
203
369
|
await Promise.all(workers);
|
|
204
370
|
}
|
|
205
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Orchestrates a full backfill run: enumerate repos, filter to pending, download, and index.
|
|
373
|
+
*
|
|
374
|
+
* Operates in one of three modes based on config:
|
|
375
|
+
* - **Pinned repos** — backfill only the DIDs listed in `config.repos`
|
|
376
|
+
* - **Full network** — enumerate every active repo on the relay via `listRepos`
|
|
377
|
+
* - **Collection signal** (default) — use `listReposByCollection` to discover repos that
|
|
378
|
+
* contain records in the configured signal collections, falling back to `listRepos`
|
|
379
|
+
* if the relay doesn't support collection-scoped enumeration
|
|
380
|
+
*
|
|
381
|
+
* After the initial pass, failed repos are retried with exponential backoff
|
|
382
|
+
* (up to `config.maxRetries` attempts). The run emits structured log events for
|
|
383
|
+
* monitoring via the `backfill.run` and `backfill.retry_round` event types.
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* ```ts
|
|
387
|
+
* await runBackfill({
|
|
388
|
+
* pdsUrl: 'wss://bsky.network',
|
|
389
|
+
* plcUrl: 'https://plc.directory',
|
|
390
|
+
* collections: new Set(['xyz.statusphere.status']),
|
|
391
|
+
* config: {
|
|
392
|
+
* fullNetwork: false,
|
|
393
|
+
* parallelism: 10,
|
|
394
|
+
* fetchTimeout: 30,
|
|
395
|
+
* maxRetries: 5,
|
|
396
|
+
* },
|
|
397
|
+
* })
|
|
398
|
+
* ```
|
|
399
|
+
*/
|
|
206
400
|
export async function runBackfill(opts) {
|
|
207
401
|
const { pdsUrl, collections, config } = opts;
|
|
208
402
|
plcUrl = opts.plcUrl;
|
|
@@ -217,26 +411,30 @@ export async function runBackfill(opts) {
|
|
|
217
411
|
}
|
|
218
412
|
}
|
|
219
413
|
else if (config.fullNetwork) {
|
|
220
|
-
for
|
|
221
|
-
|
|
414
|
+
for (const url of [pdsUrl, ...(opts.extraPdsUrls ?? [])]) {
|
|
415
|
+
for await (const repo of listRepos(url)) {
|
|
416
|
+
dids.add(repo.did);
|
|
417
|
+
}
|
|
222
418
|
}
|
|
223
419
|
}
|
|
224
420
|
else {
|
|
225
|
-
for (const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
catch (err) {
|
|
232
|
-
// Fall back to listRepos if listReposByCollection not supported
|
|
233
|
-
if (err.message.includes('400') || err.message.includes('401') || err.message.includes('501')) {
|
|
234
|
-
for await (const repo of listRepos(pdsUrl)) {
|
|
421
|
+
for (const url of [pdsUrl, ...(opts.extraPdsUrls ?? [])]) {
|
|
422
|
+
for (const col of signalCollections) {
|
|
423
|
+
try {
|
|
424
|
+
for await (const repo of listReposByCollection(url, col)) {
|
|
235
425
|
dids.add(repo.did);
|
|
236
426
|
}
|
|
237
|
-
break;
|
|
238
427
|
}
|
|
239
|
-
|
|
428
|
+
catch (err) {
|
|
429
|
+
// Fall back to listRepos if listReposByCollection not supported
|
|
430
|
+
if (err.message.includes('400') || err.message.includes('401') || err.message.includes('501')) {
|
|
431
|
+
for await (const repo of listRepos(url)) {
|
|
432
|
+
dids.add(repo.did);
|
|
433
|
+
}
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
throw err;
|
|
437
|
+
}
|
|
240
438
|
}
|
|
241
439
|
}
|
|
242
440
|
}
|
|
@@ -244,7 +442,7 @@ export async function runBackfill(opts) {
|
|
|
244
442
|
const pending = [];
|
|
245
443
|
for (const did of dids) {
|
|
246
444
|
const status = await getRepoStatus(did);
|
|
247
|
-
if (status
|
|
445
|
+
if (backfillEligible(status)) {
|
|
248
446
|
if (!status)
|
|
249
447
|
await setRepoStatus(did, 'pending');
|
|
250
448
|
pending.push(did);
|
|
@@ -267,7 +465,7 @@ export async function runBackfill(opts) {
|
|
|
267
465
|
parallelism: config.parallelism,
|
|
268
466
|
status: 'success',
|
|
269
467
|
});
|
|
270
|
-
return;
|
|
468
|
+
return 0;
|
|
271
469
|
}
|
|
272
470
|
// 3. Backfill with worker pool
|
|
273
471
|
let totalRecords = 0;
|
|
@@ -291,7 +489,7 @@ export async function runBackfill(opts) {
|
|
|
291
489
|
retryRound++;
|
|
292
490
|
// Wait until the earliest retry_after has passed
|
|
293
491
|
const now = Math.floor(Date.now() / 1000);
|
|
294
|
-
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]);
|
|
492
|
+
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]));
|
|
295
493
|
const earliest = rows[0]?.earliest ? Number(rows[0].earliest) : 0;
|
|
296
494
|
if (earliest > now) {
|
|
297
495
|
await new Promise((resolve) => setTimeout(resolve, (earliest - now) * 1000));
|
|
@@ -325,4 +523,5 @@ export async function runBackfill(opts) {
|
|
|
325
523
|
retry_rounds: retryRound,
|
|
326
524
|
status: failedCount > 0 ? 'partial' : 'success',
|
|
327
525
|
});
|
|
526
|
+
return totalRecords;
|
|
328
527
|
}
|
package/dist/car.d.ts
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* CAR (Content Addressable aRchive) parser.
|
|
3
|
+
*
|
|
4
|
+
* CAR files bundle content-addressed blocks into a single binary container.
|
|
5
|
+
* They're used by the AT Protocol firehose (`com.atproto.sync.getRepo`) to
|
|
6
|
+
* deliver entire repos and by commit events to deliver individual changes.
|
|
7
|
+
*
|
|
8
|
+
* Format: `varint(headerLen) | CBOR(header) | block*`
|
|
9
|
+
* Each block: `varint(blockLen) | CID | data`
|
|
10
|
+
*
|
|
11
|
+
* @see https://ipld.io/specs/transport/car/carv1/
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
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<{
|
|
2
46
|
roots: string[];
|
|
3
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).
|
|
55
|
+
*
|
|
56
|
+
* @param carBytes - Raw CAR file bytes (e.g. from `getRepo` or a firehose commit)
|
|
57
|
+
* @returns `roots` — ordered list of root CID strings; `blocks` — lazy block map
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseCarFrame(carBytes: Uint8Array): {
|
|
60
|
+
roots: string[];
|
|
61
|
+
blocks: LazyBlockMap;
|
|
4
62
|
};
|
|
5
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":"
|
|
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"}
|