@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.
@@ -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 {
@@ -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,CAkK/G;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAkIrE"}
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 !== 'active') {
406
+ if (backfillEligible(status)) {
389
407
  if (!status)
390
408
  await setRepoStatus(did, 'pending');
391
409
  pending.push(did);
@@ -1 +1 @@
1
- {"version":3,"file":"dev-entry.d.ts","sourceRoot":"","sources":["../src/dev-entry.ts"],"names":[],"mappings":"AAgHA,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"}
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
- const cursor = await getCursor('relay');
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
  *
@@ -1 +1 @@
1
- {"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"AAuKA;;;;;;;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,CAoDxE"}
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(lastSeq));
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
- lastSeq = body.value.seq;
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
- const cursor = await getCursor('relay');
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatk/hatk",
3
- "version": "0.0.1-alpha.67",
3
+ "version": "0.0.1-alpha.69",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "hatk": "dist/cli.js"