trypost/app/Support/Social/PublishCheckpoint.php

52 lines
1.4 KiB
PHP
Raw Permalink Normal View History

Resume in-flight Instagram and TikTok publishes without duplicates (#281) * Improve asynchronous social publishing reliability * fix: resume asynchronous social publishes * fix: preserve publish checkpoints across retries * fix: harden resumable publish lifecycle * fix: clean retry resources on terminal failures * test: cover resumable social publishing edge cases * feat: add failed post retry command * chore: remove retry command ai rule * fix: require confirmation for post retries * chore: remove ai rules index * chore: remove ai social rule * refactor: clarify TikTok derivative path validation * refactor: simplify social publishing retries * refactor: further simplify social publishing retries * refactor: retry all failed post platforms * style: import throwable in social retries * refactor: decouple TikTok cleanup from image format * refactor: extract missing publish scopes * refactor: encapsulate missing scope failure * fix: resume failed publishes and treat Instagram rate limits as transient Keep TikTok/Instagram checkpoints on posts:retry so a manual retry does not start a duplicate remote post. Classify Meta BUC 400s on Instagram status polls as retryable via GraphError. Co-authored-by: Cursor <cursoragent@cursor.com> * test: cover resume paths and transient Instagram rate limits Co-authored-by: Cursor <cursoragent@cursor.com> * fix: resume posts:retry only for in-flight publish failures Co-authored-by: Cursor <cursoragent@cursor.com> * fix: resume posts:retry via ErrorCategory instead of string lists Co-authored-by: Cursor <cursoragent@cursor.com> * fix: handle Instagram PUBLISHED and EXPIRED container statuses Treat EXPIRED as a terminal server error so posts:retry starts over, and complete already-published containers without a second media_publish. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: recover published Instagram stories from /stories Stories are not on GET /{ig-user-id}/media. Resume a PUBLISHED story container from the stories edge so we do not bind a feed post id. Co-authored-by: Cursor <cursoragent@cursor.com> * test: cover Instagram EXPIRED retry and published recovery paths Co-authored-by: Cursor <cursoragent@cursor.com> * fix: stop guessing Instagram media ids from recent /media Co-authored-by: Cursor <cursoragent@cursor.com> * fix: checkpoint TikTok publish_id and keep in-flight photo derivatives Persist publish_id right after /init/ so a crash can resume without a second publish. Keep hosted photos while that id is resumable, including token expiry on status fetch; prune only after success or a confirmed remote failure. Co-authored-by: Cursor <cursoragent@cursor.com> * test: cover remaining TikTok in-flight derivative edge cases Guard the empty publish_id prune path, account guards without a checkpoint, and video status 401 after /init/. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: map TikTok publish statuses with an official enum Use PublishStatus for status/fetch values from the Content Posting API. Keep only the documented cases, including FAILED as the terminal failure. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor: share in-flight publish checkpoint keys Read TikTok and Instagram resume state through one helper so publishers, posts:retry, and derivative cleanup agree on the same keys. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: retry Instagram media_publish after transient Graph failures A 500/code 2 after Meta already published left the job Failed as unknown. Treat that as still-processing so resume can confirm PUBLISHED instead of posting again. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: resume Instagram publish after dropped Graph connections A timeout or connection reset after Meta already published was marked unknown. Treat it as still-processing so resume can confirm PUBLISHED instead of posting again. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 18:34:53 +00:00
<?php
declare(strict_types=1);
namespace App\Support\Social;
/**
* Shared keys and readers for in-flight publish checkpoints on
* PostPlatform.error_context. Used by the publishers, the TikTok
* derivative cleaner, and posts:retry.
*/
final class PublishCheckpoint
{
public const string TIKTOK_PUBLISH_ID = 'tiktok_publish_id';
public const string TIKTOK_DERIVATIVE_PATHS = 'tiktok_derivative_paths';
public const string INSTAGRAM_WORKFLOW = 'instagram_workflow';
/**
* @param array<string, mixed>|null $context
*/
public static function tiktokPublishId(?array $context): ?string
{
$value = data_get($context, self::TIKTOK_PUBLISH_ID);
return is_string($value) && $value !== '' ? $value : null;
}
/**
* @param array<string, mixed>|null $context
* @return array<array-key, mixed>
*/
public static function tiktokDerivativePaths(?array $context): array
{
$paths = data_get($context, self::TIKTOK_DERIVATIVE_PATHS, []);
return is_array($paths) ? $paths : [];
}
/**
* @param array<string, mixed>|null $context
* @return array<string, mixed>|null
*/
public static function instagramWorkflow(?array $context): ?array
{
$workflow = data_get($context, self::INSTAGRAM_WORKFLOW);
return is_array($workflow) && $workflow !== [] ? $workflow : null;
}
}