Skip to content

Commit b5ace87

Browse files
authored
Surface per-post publish failures in the block editor (#183)
1 parent 5729c5e commit b5ace87

7 files changed

Lines changed: 380 additions & 10 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
The editor now shows a notice when sharing a post to Bluesky fails, including whether it will be retried automatically or needs the post to be updated again.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<?php return array('dependencies' => array('react-jsx-runtime', 'wp-components', 'wp-core-data', 'wp-data', 'wp-edit-post', 'wp-editor', 'wp-i18n', 'wp-plugins'), 'version' => '79298abf5597585035c7');
1+
<?php return array('dependencies' => array('react-jsx-runtime', 'wp-components', 'wp-core-data', 'wp-data', 'wp-edit-post', 'wp-editor', 'wp-i18n', 'wp-plugins'), 'version' => '96f1cc2d94607e1a8aae');

build/editor-plugin/plugin.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

includes/class-atmosphere.php

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ class Atmosphere {
5858
*/
5959
private const META_PUBLISH_RETRIES = '_atmosphere_publish_retries';
6060

61+
/**
62+
* Post meta key holding the most recent publish/update failure.
63+
*
64+
* Written by the cron workers when an attempt fails, cleared on the
65+
* next success. Shape: `code`, `message` (sanitized + truncated —
66+
* PDS-supplied strings can carry hostile bytes), `retrying` (whether
67+
* the backoff ladder scheduled another attempt), `time`. Exposed to
68+
* the block editor via the read-only `atmosphere_publish_error`
69+
* REST field so authors can see that a share failed instead of the
70+
* failure vanishing into a WP_DEBUG-gated log line.
71+
*
72+
* @var string
73+
*/
74+
private const META_LAST_PUBLISH_ERROR = '_atmosphere_last_publish_error';
75+
6176
/**
6277
* Backoff ladder for transient publish/update failures, in seconds.
6378
*
@@ -715,9 +730,13 @@ public function on_status_change( string $new_status, string $old_status, \WP_Po
715730
* dead retry event (disconnect cleared the queue, the post
716731
* was trashed mid-ladder, a cron event was lost) would
717732
* silently shrink — or zero out — the ladder of the next
718-
* publish attempt.
733+
* publish attempt. The stale failure record goes with it:
734+
* on a cleanup transition the delete worker never routes
735+
* through the retry helper, so an old "share failed" notice
736+
* would otherwise stick to a post that is no longer shared.
719737
*/
720738
\delete_post_meta( $post->ID, self::META_PUBLISH_RETRIES );
739+
\delete_post_meta( $post->ID, self::META_LAST_PUBLISH_ERROR );
721740

722741
if ( $is_publishable ) {
723742
\wp_clear_scheduled_hook( 'atmosphere_delete_post', array( $post->ID ) );
@@ -1415,11 +1434,15 @@ public function register_share_meta(): void {
14151434
}
14161435

14171436
/**
1418-
* Register a read-only REST field with the published post's Bluesky URL.
1419-
*
1420-
* Lets the block-editor panel show a "View on Bluesky" link once the
1421-
* post has been shared, without exposing internal AT-URI meta keys.
1422-
* Empty until the post has a Bluesky record.
1437+
* Register the read-only REST fields backing the editor panel.
1438+
*
1439+
* `atmosphere_url` lets the panel show a "View on Bluesky" link once
1440+
* the post has been shared, without exposing internal AT-URI meta
1441+
* keys (empty until the post has a Bluesky record).
1442+
* `atmosphere_publish_error` carries the most recent share failure
1443+
* (null when the last attempt succeeded) so the panel can tell the
1444+
* author a share failed instead of the failure vanishing into a
1445+
* WP_DEBUG-gated log line. Both are edit-context only.
14231446
*/
14241447
public function register_share_status_field(): void {
14251448
foreach ( get_supported_post_types() as $post_type ) {
@@ -1440,6 +1463,51 @@ public function register_share_status_field(): void {
14401463
),
14411464
)
14421465
);
1466+
1467+
\register_rest_field(
1468+
$post_type,
1469+
'atmosphere_publish_error',
1470+
array(
1471+
'get_callback' => static function ( $post_arr ) {
1472+
$error = \get_post_meta( (int) $post_arr['id'], self::META_LAST_PUBLISH_ERROR, true );
1473+
1474+
if ( ! \is_array( $error ) || empty( $error['code'] ) ) {
1475+
return null;
1476+
}
1477+
1478+
return array(
1479+
'code' => (string) $error['code'],
1480+
'message' => (string) ( $error['message'] ?? '' ),
1481+
'retrying' => ! empty( $error['retrying'] ),
1482+
'time' => (int) ( $error['time'] ?? 0 ),
1483+
);
1484+
},
1485+
'update_callback' => null,
1486+
'schema' => array(
1487+
'type' => array( 'object', 'null' ),
1488+
'description' => \__( 'The most recent Bluesky sharing failure for this post, null when the last attempt succeeded.', 'atmosphere' ),
1489+
'context' => array( 'edit' ),
1490+
'properties' => array(
1491+
'code' => array(
1492+
'type' => 'string',
1493+
'description' => \__( 'Machine-readable failure code.', 'atmosphere' ),
1494+
),
1495+
'message' => array(
1496+
'type' => 'string',
1497+
'description' => \__( 'Human-readable failure message.', 'atmosphere' ),
1498+
),
1499+
'retrying' => array(
1500+
'type' => 'boolean',
1501+
'description' => \__( 'Whether another automatic attempt is scheduled.', 'atmosphere' ),
1502+
),
1503+
'time' => array(
1504+
'type' => 'integer',
1505+
'description' => \__( 'Unix timestamp of the failed attempt.', 'atmosphere' ),
1506+
),
1507+
),
1508+
),
1509+
)
1510+
);
14431511
}
14441512
}
14451513

@@ -1545,6 +1613,15 @@ static function ( int $post_id ): void {
15451613
? Publisher::update_post( $post )
15461614
: Publisher::publish_post( $post );
15471615
self::log_cron_error( 'delete_post_publishable_reconcile', $post_id, $result );
1616+
1617+
/*
1618+
* Same lifecycle as the publish/update workers: a
1619+
* successful reconcile clears the retry counter and
1620+
* the stale failure record, and a transient failure
1621+
* re-queues (as an update — the worker re-checks
1622+
* state when the retry fires).
1623+
*/
1624+
self::maybe_schedule_publish_retry( 'atmosphere_update_post', $post_id, $result );
15481625
if ( ! \is_wp_error( $result ) ) {
15491626
self::clear_visibility_cleanup_marker( $post );
15501627
}
@@ -1910,7 +1987,14 @@ public static function log_reconcile_cleanup_error( int $post_id, $result ): voi
19101987
* @param mixed $result Publisher result: array on success, `WP_Error` on failure.
19111988
*/
19121989
private static function maybe_schedule_publish_retry( string $hook, int $post_id, $result ): void {
1913-
if ( ! \is_wp_error( $result ) || ! self::is_transient_publish_error( $result ) ) {
1990+
if ( ! \is_wp_error( $result ) ) {
1991+
\delete_post_meta( $post_id, self::META_PUBLISH_RETRIES );
1992+
\delete_post_meta( $post_id, self::META_LAST_PUBLISH_ERROR );
1993+
return;
1994+
}
1995+
1996+
if ( ! self::is_transient_publish_error( $result ) ) {
1997+
self::record_publish_error( $post_id, $result, false );
19141998
\delete_post_meta( $post_id, self::META_PUBLISH_RETRIES );
19151999
return;
19162000
}
@@ -1948,6 +2032,7 @@ private static function maybe_schedule_publish_retry( string $hook, int $post_id
19482032
* would misread as a failure of the ladder the operator
19492033
* deliberately switched off.
19502034
*/
2035+
self::record_publish_error( $post_id, $result, false );
19512036
\delete_post_meta( $post_id, self::META_PUBLISH_RETRIES );
19522037

19532038
if ( ! empty( $delays ) ) {
@@ -1964,6 +2049,7 @@ private static function maybe_schedule_publish_retry( string $hook, int $post_id
19642049
return;
19652050
}
19662051

2052+
self::record_publish_error( $post_id, $result, true );
19672053
\update_post_meta( $post_id, self::META_PUBLISH_RETRIES, $attempts + 1 );
19682054
\wp_schedule_single_event(
19692055
\time() + $delays[ $attempts ],
@@ -1972,6 +2058,31 @@ private static function maybe_schedule_publish_retry( string $hook, int $post_id
19722058
);
19732059
}
19742060

2061+
/**
2062+
* Persist a publish failure to post meta for the editor to surface.
2063+
*
2064+
* The message flows from `WP_Error::get_error_message()` and can carry
2065+
* PDS-supplied bytes, so it is sanitized and truncated before storage
2066+
* — same caution as `log_cron_error()`, but for a value that ends up
2067+
* rendered in the block editor rather than a log line.
2068+
*
2069+
* @param int $post_id Post the attempt ran for.
2070+
* @param \WP_Error $error The failure.
2071+
* @param bool $retrying Whether the backoff ladder scheduled another attempt.
2072+
*/
2073+
private static function record_publish_error( int $post_id, \WP_Error $error, bool $retrying ): void {
2074+
\update_post_meta(
2075+
$post_id,
2076+
self::META_LAST_PUBLISH_ERROR,
2077+
array(
2078+
'code' => (string) $error->get_error_code(),
2079+
'message' => truncate_text( sanitize_text( $error->get_error_message() ), 300 ),
2080+
'retrying' => $retrying,
2081+
'time' => \time(),
2082+
)
2083+
);
2084+
}
2085+
19752086
/**
19762087
* Whether a publish failure is worth retrying.
19772088
*

src/editor-plugin/plugin.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,12 @@ const atmosphereIcon = (
6161
* @return {React.JSX.Element|null} The panel, or null for sync blocks.
6262
*/
6363
const EditorPlugin = () => {
64-
const { postType, sharedUrl } = useSelect( ( select ) => {
64+
const { postType, sharedUrl, publishError } = useSelect( ( select ) => {
6565
const editor = select( editorStore );
6666
return {
6767
postType: editor.getCurrentPostType(),
6868
sharedUrl: editor.getCurrentPost()?.atmosphere_url,
69+
publishError: editor.getCurrentPost()?.atmosphere_publish_error,
6970
};
7071
}, [] );
7172

@@ -147,6 +148,30 @@ const EditorPlugin = () => {
147148
) }
148149
</Notice>
149150
) }
151+
152+
{ /* The last share attempt failed. The record comes from the
153+
`atmosphere_publish_error` REST field (cleared server-side on
154+
the next success), so the notice disappears once a share goes
155+
through. Retrying=true means the backoff ladder has another
156+
attempt queued; otherwise the author's update is the retry. */ }
157+
{ publishError && enabled && (
158+
<Notice status="error" isDismissible={ false }>
159+
{ publishError.retrying
160+
? __(
161+
'Sharing to Bluesky failed. Your site will retry automatically.',
162+
'atmosphere'
163+
)
164+
: __(
165+
'Sharing to Bluesky failed. Update the post to try again.',
166+
'atmosphere'
167+
) }
168+
{ publishError.message && (
169+
<p style={ { marginBottom: 0 } }>
170+
<small>{ publishError.message }</small>
171+
</p>
172+
) }
173+
</Notice>
174+
) }
150175
</SettingsPanel>
151176
);
152177
};

0 commit comments

Comments
 (0)