Files
WP-Site-Sync/includes/class-state.php
2025-12-14 16:58:52 -06:00

212 lines
6.7 KiB
PHP

<?php
namespace SiteSync;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Handles the persistent state for the Site Sync plugin, including sync checkpoints, tombstones, and mappings.
*/
class State {
public const OPTION_KEY = 'site_sync_state';
private const TOMBSTONE_LIMIT = 200;
/**
* Default state structure for Site Sync plugin.
*
* @var array
*/
private $defaults = array(
'last_sent' => array(
'posts' => array(
'modified' => null,
'id' => 0,
),
'terms' => array(
'modified' => null,
'id' => 0,
),
'media' => array(
'modified' => null,
'id' => 0,
),
),
'last_received' => array(
'posts' => array(
'modified' => null,
'id' => 0,
),
'terms' => array(
'modified' => null,
'id' => 0,
),
'media' => array(
'modified' => null,
'id' => 0,
),
),
'last_counts' => array(
'sent' => array(
'posts' => 0,
'terms' => 0,
'media' => 0,
'tombstones' => 0,
),
'received' => array(
'applied' => 0,
'skipped' => 0,
'errors' => 0,
),
),
'mappings' => array(
'posts' => array(), // phpcs:ignore local_id => array( 'peer' => peer_id, 'peer_site' => site_uuid )
'terms' => array(),
'media' => array(),
),
'last_deleted' => array(
'posts' => 0,
'terms' => 0,
'media' => 0,
),
'tombstones' => array(),
'last_run' => null,
'last_error' => null,
);
/**
* Retrieves the current persistent state for the Site Sync plugin.
*
* @return array The current state array merged with defaults.
*/
public function get(): array {
$stored = get_option( self::OPTION_KEY, array() );
$data = is_array( $stored ) ? $stored : array();
return array_merge( $this->defaults, $data );
}
/**
* Updates the persistent state with the provided data.
*
* @param array $data The data to merge into the current state.
*/
public function update( array $data ): void {
$state = $this->get();
$state = array_merge( $state, $data );
update_option( self::OPTION_KEY, $state );
}
/**
* Records the results of a sync run, including summary counts and errors.
*
* @param array $summary An array containing 'counts' and optionally 'error' from the sync run.
*/
public function record_run( array $summary ): void {
$state = $this->get();
$state['last_run'] = current_time( 'mysql' );
$state['last_error'] = $summary['error'] ?? null;
if ( isset( $summary['counts'] ) && is_array( $summary['counts'] ) ) {
$state['last_counts'] = $this->merge_counts( $summary['counts'] );
}
update_option( self::OPTION_KEY, $state );
}
/**
* Reset checkpoints and tombstone queue to force a full resend on next sync.
*/
public function reset(): void {
$state = $this->get();
$state['last_sent'] = $this->defaults['last_sent'];
$state['last_received'] = $this->defaults['last_received'];
$state['tombstones'] = array();
$state['last_counts'] = $this->defaults['last_counts'];
$state['last_error'] = null;
update_option( self::OPTION_KEY, $state );
}
/**
* Adds a tombstone item to the queue, ensuring no duplicates based on external_id and type.
*
* @param array $item The tombstone item to enqueue. Should contain 'external_id', 'type', and optionally 'taxonomy'.
*
* @return void
*/
public function enqueue_tombstone( array $item ): void {
// Avoid duplicates: check if same external_id/type already queued.
$state = $this->get();
if ( ! empty( $item['external_id'] ) && ! empty( $item['type'] ) ) {
$existing = array_filter(
$state['tombstones'],
function ( $t ) use ( $item ) {
return isset( $t['external_id'], $t['type'] )
&& $t['external_id'] === $item['external_id']
&& $t['type'] === $item['type']
&& ( ! isset( $item['taxonomy'] ) || ( $t['taxonomy'] ?? '' ) === ( $item['taxonomy'] ?? '' ) );
}
);
if ( ! empty( $existing ) ) {
return;
}
}
$state['tombstones'][] = $item;
if ( count( $state['tombstones'] ) > self::TOMBSTONE_LIMIT ) {
$state['tombstones'] = array_slice( $state['tombstones'], -self::TOMBSTONE_LIMIT );
}
update_option( self::OPTION_KEY, $state );
}
/**
* Removes and returns up to $limit tombstone items from the queue.
*
* @param int $limit The maximum number of tombstone items to consume.
* @return array The consumed tombstone items.
*/
public function consume_tombstones( int $limit = 50 ): array {
$state = $this->get();
$queue = $state['tombstones'] ?? array();
$items = array_slice( $queue, 0, $limit );
$state['tombstones'] = array_slice( $queue, $limit );
update_option( self::OPTION_KEY, $state );
return $items;
}
/**
* Returns up to $limit tombstone items from the queue without removing them.
*
* @param int $limit The maximum number of tombstone items to return.
* @return array The tombstone items.
*/
public function peek_tombstones( int $limit = 50 ): array {
$state = $this->get();
$queue = $state['tombstones'] ?? array();
return array_slice( $queue, 0, $limit );
}
/**
* Merges provided counts with the default counts, ensuring all values are integers.
*
* @param array $counts The counts to merge, containing 'sent' and 'received' arrays.
* @return array The merged counts array.
*/
private function merge_counts( array $counts ): array {
$defaults = $this->defaults['last_counts'];
$sent = $counts['sent'] ?? array();
$received = $counts['received'] ?? array();
$mergedSent = array_merge( $defaults['sent'], array_map( 'intval', $sent ) );
$mergedReceived = array_merge( $defaults['received'], array_map( 'intval', $received ) );
return array(
'sent' => $mergedSent,
'received' => $mergedReceived,
);
}
}