fix: tighten catchup balloon filter + suppress unhandled rejection in write queue

Address Codex P1 review feedback:
- Balloon filter: only skip when associatedMessageType OR balloonBundleId
  is set alongside associatedMessageGuid. Threaded replies use
  threadOriginatorGuid and are unaffected.
- Write queue: .catch(() => {}) on the cleanup promise so a rejected
  next doesn't surface as an unhandled rejection in Node 22+.
This commit is contained in:
Lobster
2026-04-15 19:38:58 -07:00
parent 85aa463013
commit 5ed919ac71
2 changed files with 22 additions and 8 deletions

View File

@@ -498,19 +498,27 @@ async function runBlueBubblesCatchupInner(
continue;
}
// Skip balloon/tapback/reaction events: they carry an
// Skip tapback/reaction/balloon events. These carry an
// `associatedMessageGuid` pointing at the parent text message and
// have a different `guid` of their own. The live webhook path handles
// them via the debouncer, which coalesces balloons with their parent.
// balloons via the debouncer, which coalesces them with their parent.
// Without debouncing here, replaying a balloon would dispatch it as a
// standalone message — producing a duplicate reply to the parent.
//
// Guard: only skip when `associatedMessageType` is set (tapbacks and
// reactions — e.g., "like", 2000) OR `balloonBundleId` is set (URL
// previews, stickers). iMessage threaded replies use a separate
// `threadOriginatorGuid` field and do NOT set either of these, so
// they pass through for correct catchup replay.
const assocGuid =
typeof rec.associatedMessageGuid === "string"
? rec.associatedMessageGuid.trim()
: typeof rec.associated_message_guid === "string"
? rec.associated_message_guid.trim()
: "";
if (assocGuid) {
const assocType = rec.associatedMessageType ?? rec.associated_message_type;
const balloonId = typeof rec.balloonBundleId === "string" ? rec.balloonBundleId.trim() : "";
if (assocGuid && (assocType != null || balloonId)) {
continue;
}

View File

@@ -170,11 +170,17 @@ export function createPersistentDedupe(options: PersistentDedupeOptions): Persis
const prev = fileWriteQueues.get(filePath) ?? Promise.resolve();
const next = prev.then(fn, fn);
fileWriteQueues.set(filePath, next);
void next.finally(() => {
if (fileWriteQueues.get(filePath) === next) {
fileWriteQueues.delete(filePath);
}
});
// Cleanup: remove the queue entry once this link settles, but only if
// no newer work was chained after us. The `.catch(() => {})` prevents
// an unhandled rejection when `next` rejects — callers still observe
// the rejection through the returned `next` promise directly.
next
.finally(() => {
if (fileWriteQueues.get(filePath) === next) {
fileWriteQueues.delete(filePath);
}
})
.catch(() => {});
return next;
}