···11+---
22+"@atmo-dev/contrail-appview": patch
33+---
44+55+Persist the jetstream ingest cursor before the identity-refresh tail in `runIngestCycle`.
66+77+`saveCursor` previously ran after `refreshStaleIdentities`, whose per-DID network calls can run long. If the ingest isolate was aborted (e.g. a scheduled-invocation deadline) before the save, the cursor never advanced and the next cycle re-drained the same jetstream window indefinitely. Records are durably applied before this point, so the cursor is now saved first; identity refresh is idempotent and staleness-driven, so deferring it past the save is safe.
+19-10
packages/contrail-appview/src/core/jetstream.ts
···385385 log.log(`[ingest] applied ${identityUpdates.size} identity event(s)`);
386386 }
387387388388- // Refresh stale/missing identities for DIDs in this batch
389389- const uniqueDids = [...new Set(events.map((e) => e.did))];
390390- if (uniqueDids.length > 0) {
391391- try {
392392- await refreshStaleIdentities(db, uniqueDids, config);
393393- } catch (err) {
394394- log.warn(`Identity refresh failed: ${err}`);
395395- }
396396- }
397397-388388+ // Persist the cursor BEFORE the best-effort enrichment tail below. Records are
389389+ // already durably applied (applyEvents) and handle changes recorded, so the
390390+ // cursor's forward progress is real and must be committed now. The steps that
391391+ // follow — refreshStaleIdentities especially — make per-DID network calls and
392392+ // can run long; if the cron isolate is aborted (e.g. a scheduled-invocation
393393+ // deadline) while they run, an un-saved cursor makes the next cycle re-drain the
394394+ // identical window forever. Identity refresh is idempotent and staleness-driven,
395395+ // so deferring it past the save costs nothing.
398396 if (lastCursor !== null) {
399397 await saveCursor(db, lastCursor);
400398 log.log(
···404402 );
405403 } else {
406404 log.log(`[ingest] no cursor returned from subscription; not saving`);
405405+ }
406406+407407+ // Refresh stale/missing identities for DIDs in this batch (best-effort; runs
408408+ // after the cursor save so its network latency can't strand forward progress).
409409+ const uniqueDids = [...new Set(events.map((e) => e.did))];
410410+ if (uniqueDids.length > 0) {
411411+ try {
412412+ await refreshStaleIdentities(db, uniqueDids, config);
413413+ } catch (err) {
414414+ log.warn(`Identity refresh failed: ${err}`);
415415+ }
407416 }
408417409418 // Newly-discovered DIDs: ask Constellation for back-edges so they