@hatk/hatk 0.0.1-alpha.67 → 0.0.1-alpha.68

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.
@@ -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.68",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "hatk": "dist/cli.js"