From 8fbad755822d0f5cefa4a5d1f6bbd0867cddc40d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Thu, 23 Apr 2026 23:07:58 +0200 Subject: [PATCH] fix: handle concurrent webhooks --- cms/schemaTypes/ama.ts | 4 +++- server/api/publish-ama.post.ts | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/cms/schemaTypes/ama.ts b/cms/schemaTypes/ama.ts index a4997f3e..1e7fe1f4 100644 --- a/cms/schemaTypes/ama.ts +++ b/cms/schemaTypes/ama.ts @@ -60,6 +60,7 @@ export default defineType({ const statusEmoji: Record = { 'draft': '📝', 'ready': '🚀', + 'publishing': '⏳', 'partially-published': '⚠️', 'published': '✅', 'failed': '❌', @@ -167,13 +168,14 @@ export default defineType({ list: [ { title: 'Draft', value: 'draft' }, { title: 'Ready to publish', value: 'ready' }, + { title: 'Publishing', value: 'publishing' }, { title: 'Partially published', value: 'partially-published' }, { title: 'Published', value: 'published' }, { title: 'Failed', value: 'failed' }, ], }, initialValue: 'draft', - readOnly: ({ document }) => document?.publishStatus === 'ready' || document?.publishStatus === 'published', + readOnly: ({ document }) => document?.publishStatus === 'ready' || document?.publishStatus === 'publishing' || document?.publishStatus === 'published', }), defineField({ name: 'generatedImage', diff --git a/server/api/publish-ama.post.ts b/server/api/publish-ama.post.ts index a2c72b32..b7b58948 100644 --- a/server/api/publish-ama.post.ts +++ b/server/api/publish-ama.post.ts @@ -196,10 +196,50 @@ export default defineEventHandler(async event => { } } + if (document.publishStatus !== 'ready') { + console.log('Webhook ignored: document is not ready to publish (another invocation may be in flight)', { + id: body._id, + status: document.publishStatus, + }) + return { + success: true, + message: 'Document not in ready state', + status: document.publishStatus, + } + } + if (!document.posts || document.posts.length === 0) { throw new Error('No posts found for publishing') } + // Claim exclusive ownership of this publish run by atomically transitioning + // `publishStatus` from `ready` to `publishing`, conditional on the revision we read. + // Any concurrent webhook delivery will fail this patch (revision mismatch) and bail. + // This prevents races where two invocations both publish to the same platforms and + // one deletes the video asset from under the other. + try { + await sanity.client + .patch(document._id) + .ifRevisionId(document._rev) + .set({ + publishStatus: 'publishing', + lastWebhookEvent: { timestamp, signature }, + }) + .commit() + } + catch (error) { + // If the revision no longer matches, another webhook has already claimed this run. + console.log('Webhook ignored: failed to claim publish lock (another invocation has it)', { + id: body._id, + error: String(error), + }) + return { + success: true, + message: 'Another publish run is in progress', + status: 'publishing', + } + } + // Publish to platforms, skipping any that already have a published link const results = await publishToPlatforms(event, document) -- 2.54.0