@hatk/hatk 0.0.1-alpha.67 → 0.0.1-alpha.69
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/backfill.d.ts +9 -0
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +19 -1
- package/dist/dev-entry.d.ts.map +1 -1
- package/dist/dev-entry.js +5 -1
- package/dist/indexer.d.ts +20 -0
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +53 -3
- package/dist/main.js +8 -1
- package/package.json +1 -1
package/dist/backfill.d.ts
CHANGED
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
* Exported for tests.
|
|
9
9
|
*/
|
|
10
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;
|
|
11
20
|
import type { BackfillConfig } from './config.ts';
|
|
12
21
|
/** Options passed to {@link runBackfill}. */
|
|
13
22
|
interface BackfillOpts {
|
package/dist/backfill.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;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,
|
|
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
|
@@ -15,6 +15,17 @@ import { isPrivateCollection } from "./private-collections.js";
|
|
|
15
15
|
export function purgeableCollections(collections) {
|
|
16
16
|
return [...collections].filter((c) => !isPrivateCollection(c));
|
|
17
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
|
+
}
|
|
18
29
|
import { emit, timer } from "./logger.js";
|
|
19
30
|
import { validateRecord } from '@bigmoves/lexicon';
|
|
20
31
|
import { getLexiconArray } from "./database/schema.js";
|
|
@@ -131,6 +142,13 @@ async function* listReposByCollection(pdsUrl, collection) {
|
|
|
131
142
|
* ```
|
|
132
143
|
*/
|
|
133
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
|
+
}
|
|
134
152
|
const elapsed = timer();
|
|
135
153
|
let count = 0;
|
|
136
154
|
let carSizeBytes;
|
|
@@ -385,7 +403,7 @@ export async function runBackfill(opts) {
|
|
|
385
403
|
const pending = [];
|
|
386
404
|
for (const did of dids) {
|
|
387
405
|
const status = await getRepoStatus(did);
|
|
388
|
-
if (status
|
|
406
|
+
if (backfillEligible(status)) {
|
|
389
407
|
if (!status)
|
|
390
408
|
await setRepoStatus(did, 'pending');
|
|
391
409
|
pending.push(did);
|
package/dist/dev-entry.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-entry.d.ts","sourceRoot":"","sources":["../src/dev-entry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dev-entry.d.ts","sourceRoot":"","sources":["../src/dev-entry.ts"],"names":[],"mappings":"AAmHA,eAAO,MAAM,OAAO,yCAKlB,CAAA;AAEF,yEAAyE;AACzE,wBAAsB,YAAY,kBAEjC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA"}
|
package/dist/dev-entry.js
CHANGED
|
@@ -72,7 +72,11 @@ if (config.oauth) {
|
|
|
72
72
|
}
|
|
73
73
|
// Start indexer
|
|
74
74
|
const collectionSet = new Set(collections);
|
|
75
|
-
|
|
75
|
+
// Same escape hatch as main.ts: boot from live instead of the saved cursor.
|
|
76
|
+
const ignoreSavedCursor = Boolean(process.env.HATK_IGNORE_SAVED_CURSOR);
|
|
77
|
+
if (ignoreSavedCursor)
|
|
78
|
+
log('[dev] HATK_IGNORE_SAVED_CURSOR set — starting indexer from live');
|
|
79
|
+
const cursor = ignoreSavedCursor ? null : await getCursor('relay');
|
|
76
80
|
startIndexer({
|
|
77
81
|
relayUrl: config.relay,
|
|
78
82
|
plcUrl: config.plc,
|
package/dist/indexer.d.ts
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/** Record the latest firehose sequence seen. Source of truth for cursor persistence and reconnect resume. */
|
|
2
|
+
export declare function noteSeq(seq: number): void;
|
|
3
|
+
/**
|
|
4
|
+
* Persist the latest firehose sequence when it has advanced past the stored
|
|
5
|
+
* cursor. Runs on a timer independent of the write path: an app whose
|
|
6
|
+
* collections rarely appear on the firehose never flushes a batch, so the
|
|
7
|
+
* flush-path cursor write alone leaves the stored cursor frozen — and a frozen
|
|
8
|
+
* cursor makes the relay replay its whole retention window at line rate
|
|
9
|
+
* (~135 GB/hour observed against bsky.network) on every boot and reconnect.
|
|
10
|
+
*/
|
|
11
|
+
export declare function checkpointCursor(): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* The cursor a reconnect should resume from: the latest seq this process has
|
|
14
|
+
* seen, falling back to the boot-time cursor before any message has arrived.
|
|
15
|
+
* Reusing the boot-time cursor on every reconnect would replay everything
|
|
16
|
+
* received since the process started.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resumeCursor(liveSeq: number | null, bootCursor: string | null | undefined): string | null | undefined;
|
|
19
|
+
/** Reset module cursor state between tests. */
|
|
20
|
+
export declare function _resetCursorStateForTests(): void;
|
|
1
21
|
/**
|
|
2
22
|
* Auto-backfill a DID's repo when first seen on the firehose.
|
|
3
23
|
*
|
package/dist/indexer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"AA4KA,6GAA6G;AAC7G,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEzC;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAStD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACpC,MAAM,GAAG,IAAI,GAAG,SAAS,CAE3B;AAED,+CAA+C;AAC/C,wBAAgB,yBAAyB,IAAI,IAAI,CAGhD;AAED;;;;;;;GAOG;AACH,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGxD;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CA8EjF;AAED,8CAA8C;AAC9C,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC/B,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAyBD;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CA4DxE"}
|
package/dist/indexer.js
CHANGED
|
@@ -12,8 +12,11 @@ import { validateRecord } from '@bigmoves/lexicon';
|
|
|
12
12
|
let buffer = [];
|
|
13
13
|
let flushTimer = null;
|
|
14
14
|
let lastSeq = null;
|
|
15
|
+
let lastPersistedSeq = null;
|
|
16
|
+
let cursorCheckpointTimer = null;
|
|
15
17
|
const BATCH_SIZE = 100;
|
|
16
18
|
const FLUSH_INTERVAL_MS = 500;
|
|
19
|
+
const CURSOR_CHECKPOINT_INTERVAL_MS = 5_000;
|
|
17
20
|
let writesSinceRebuild = 0;
|
|
18
21
|
let ftsRebuildInterval = 500;
|
|
19
22
|
// Event buffer for DIDs mid-backfill
|
|
@@ -57,8 +60,10 @@ async function flushBuffer() {
|
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
if (lastSeq !== null) {
|
|
63
|
+
const seq = lastSeq;
|
|
60
64
|
try {
|
|
61
|
-
await setCursor('relay', String(
|
|
65
|
+
await setCursor('relay', String(seq));
|
|
66
|
+
lastPersistedSeq = seq;
|
|
62
67
|
}
|
|
63
68
|
catch (err) {
|
|
64
69
|
cursorError = err.message;
|
|
@@ -134,6 +139,44 @@ function bufferWrite(item) {
|
|
|
134
139
|
scheduleFlush();
|
|
135
140
|
}
|
|
136
141
|
}
|
|
142
|
+
/** Record the latest firehose sequence seen. Source of truth for cursor persistence and reconnect resume. */
|
|
143
|
+
export function noteSeq(seq) {
|
|
144
|
+
lastSeq = seq;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Persist the latest firehose sequence when it has advanced past the stored
|
|
148
|
+
* cursor. Runs on a timer independent of the write path: an app whose
|
|
149
|
+
* collections rarely appear on the firehose never flushes a batch, so the
|
|
150
|
+
* flush-path cursor write alone leaves the stored cursor frozen — and a frozen
|
|
151
|
+
* cursor makes the relay replay its whole retention window at line rate
|
|
152
|
+
* (~135 GB/hour observed against bsky.network) on every boot and reconnect.
|
|
153
|
+
*/
|
|
154
|
+
export async function checkpointCursor() {
|
|
155
|
+
if (lastSeq === null || lastSeq === lastPersistedSeq)
|
|
156
|
+
return;
|
|
157
|
+
const seq = lastSeq;
|
|
158
|
+
try {
|
|
159
|
+
await setCursor('relay', String(seq));
|
|
160
|
+
lastPersistedSeq = seq;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
emit('indexer', 'cursor_checkpoint_error', { cursor_seq: seq, error: err.message });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The cursor a reconnect should resume from: the latest seq this process has
|
|
168
|
+
* seen, falling back to the boot-time cursor before any message has arrived.
|
|
169
|
+
* Reusing the boot-time cursor on every reconnect would replay everything
|
|
170
|
+
* received since the process started.
|
|
171
|
+
*/
|
|
172
|
+
export function resumeCursor(liveSeq, bootCursor) {
|
|
173
|
+
return liveSeq !== null ? String(liveSeq) : bootCursor;
|
|
174
|
+
}
|
|
175
|
+
/** Reset module cursor state between tests. */
|
|
176
|
+
export function _resetCursorStateForTests() {
|
|
177
|
+
lastSeq = null;
|
|
178
|
+
lastPersistedSeq = null;
|
|
179
|
+
}
|
|
137
180
|
/**
|
|
138
181
|
* Auto-backfill a DID's repo when first seen on the firehose.
|
|
139
182
|
*
|
|
@@ -275,6 +318,13 @@ export async function startIndexer(opts) {
|
|
|
275
318
|
log(`[indexer] Warmed repo status cache with ${statuses.length} entries`);
|
|
276
319
|
}
|
|
277
320
|
// startMemoryDiagnostics()
|
|
321
|
+
// Checkpoint the cursor on a timer regardless of write activity (see
|
|
322
|
+
// checkpointCursor). Guarded so reconnects don't stack intervals; unref'd so
|
|
323
|
+
// it never keeps the process alive.
|
|
324
|
+
if (!cursorCheckpointTimer) {
|
|
325
|
+
cursorCheckpointTimer = setInterval(() => void checkpointCursor(), CURSOR_CHECKPOINT_INTERVAL_MS);
|
|
326
|
+
cursorCheckpointTimer.unref?.();
|
|
327
|
+
}
|
|
278
328
|
let wsUrl = `${relayUrl}/xrpc/com.atproto.sync.subscribeRepos`;
|
|
279
329
|
if (cursor) {
|
|
280
330
|
wsUrl += `?cursor=${cursor}`;
|
|
@@ -299,7 +349,7 @@ export async function startIndexer(opts) {
|
|
|
299
349
|
ws.addEventListener('open', () => log('[indexer] Connected to relay'));
|
|
300
350
|
ws.addEventListener('close', () => {
|
|
301
351
|
log('[indexer] Disconnected, reconnecting in 3s...');
|
|
302
|
-
setTimeout(() => startIndexer(opts), 3000);
|
|
352
|
+
setTimeout(() => startIndexer({ ...opts, cursor: resumeCursor(lastSeq, opts.cursor) }), 3000);
|
|
303
353
|
});
|
|
304
354
|
return ws;
|
|
305
355
|
}
|
|
@@ -380,7 +430,7 @@ function processMessage(bytes, collections) {
|
|
|
380
430
|
return;
|
|
381
431
|
// Track sequence number for cursor
|
|
382
432
|
if (body.value.seq)
|
|
383
|
-
|
|
433
|
+
noteSeq(body.value.seq);
|
|
384
434
|
const did = body.value.repo;
|
|
385
435
|
if (!did)
|
|
386
436
|
return;
|
package/dist/main.js
CHANGED
|
@@ -199,7 +199,14 @@ log(` Feeds: ${listFeeds()
|
|
|
199
199
|
.map((f) => f.name)
|
|
200
200
|
.join(', ')}`);
|
|
201
201
|
// 6. Start indexer with cursor
|
|
202
|
-
|
|
202
|
+
// HATK_IGNORE_SAVED_CURSOR=1 boots from the live firehose instead of the saved
|
|
203
|
+
// cursor — the escape hatch when a stale cursor would make the relay replay its
|
|
204
|
+
// retention window at line rate (see checkpointCursor in indexer.ts). Unset it
|
|
205
|
+
// after one boot so later restarts resume normally.
|
|
206
|
+
const ignoreSavedCursor = Boolean(process.env.HATK_IGNORE_SAVED_CURSOR);
|
|
207
|
+
if (ignoreSavedCursor)
|
|
208
|
+
log('[main] HATK_IGNORE_SAVED_CURSOR set — starting indexer from live');
|
|
209
|
+
const cursor = ignoreSavedCursor ? null : await getCursor('relay');
|
|
203
210
|
startIndexer({
|
|
204
211
|
relayUrl: config.relay,
|
|
205
212
|
plcUrl: config.plc,
|