fix(comments): orphan-anchor reconcile + docstring (#496)
deleteEphemeralSuggestion docstring обещал инвариант «метка снимается FIRST and FATALLY» синхронно — после #399 это уже не так: fatal только ENQUEUE снятия метки, сама операция идёт в воркере с ретраями. Docstring переписан под фактическое поведение. Reconcile: воркер COMMENT_MARK_UPDATE на resolve/unresolve, обнаружив что строки комментария больше нет (hard-delete гонкой с ephemeral apply/ dismiss), теперь СНИМАЕТ осиротевшую метку вместо тихого return. Это самозаживляет тихую дивергенцию и закрывает fire-and-forget resolve/ unresolve enqueue из resolveComment. Операция идемпотентна. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+8
-2
@@ -139,13 +139,19 @@ describe('GeneralQueueProcessor — COMMENT_MARK_UPDATE (#399)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('skips (no throw) when the comment row has vanished', async () => {
|
||||
it('reconcile (#496): comment row vanished → strips the orphan anchor mark', async () => {
|
||||
const { proc, collaborationGateway, commentRepo } = makeProc();
|
||||
commentRepo.findById.mockResolvedValue(undefined);
|
||||
|
||||
await expect(
|
||||
proc.process(job({ ...base, action: 'resolve', ts: 1000 })),
|
||||
).resolves.toBeUndefined();
|
||||
expect(collaborationGateway.handleYjsEvent).not.toHaveBeenCalled();
|
||||
// A resolve/unresolve mark job whose comment row is gone leaves a silent
|
||||
// orphan; the worker self-heals by stripping the anchor instead of returning.
|
||||
expect(collaborationGateway.handleYjsEvent).toHaveBeenCalledWith(
|
||||
'deleteCommentMark',
|
||||
'page.page-1',
|
||||
{ commentId: 'c-1', user: { id: 'user-1' } },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,7 +95,8 @@ export class GeneralQueueProcessor
|
||||
* #399: apply a comment's inline-mark mirror in the collab Y.Doc, off the HTTP
|
||||
* critical path. Runs the SAME gateway path the synchronous comment.service
|
||||
* code used (byte-identical mark op):
|
||||
* - resolve / unresolve → resolveCommentMark (flip the `resolved` attribute);
|
||||
* - resolve / unresolve → resolveCommentMark (flip the `resolved` attribute),
|
||||
* OR strip an orphan anchor when the comment row has vanished (#496);
|
||||
* - delete → deleteCommentMark (strip the ephemeral-suggestion anchor #329).
|
||||
* The op is idempotent, so a BullMQ retry is safe. Throwing propagates to
|
||||
* WorkerHost → the job is retried and, on exhaustion, surfaces in failed-job
|
||||
@@ -133,7 +134,18 @@ export class GeneralQueueProcessor
|
||||
// of this resolve), skip it rather than flip the mark to a stale state.
|
||||
const comment = await this.commentRepo.findById(commentId);
|
||||
if (!comment) {
|
||||
// The comment vanished (e.g. hard-deleted) → nothing left to mirror.
|
||||
// #496 reconcile: the comment row is GONE (e.g. an ephemeral apply/dismiss
|
||||
// hard-deleted it while this resolve/unresolve mark job sat in the queue),
|
||||
// but its inline anchor may still live in the doc — a silent orphan mark
|
||||
// pointing at a comment that no longer exists. Self-heal by stripping it
|
||||
// instead of just returning: this closes the divergence the fire-and-forget
|
||||
// resolve/unresolve enqueue (comment.service resolveComment) could leave.
|
||||
// Idempotent — deleteCommentMark on an already-absent mark is a no-op.
|
||||
await this.getCollaborationGateway().handleYjsEvent(
|
||||
'deleteCommentMark',
|
||||
documentName,
|
||||
{ commentId, user },
|
||||
);
|
||||
return;
|
||||
}
|
||||
const wantResolved = action === 'resolve';
|
||||
|
||||
Reference in New Issue
Block a user