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, ); } }