key( $provider, $model ) ); if ( ! is_array( $state ) || ! isset( $state['until'], $state['delay'], $state['error_class'], $state['error'] ) ) { return null; } return array( 'until' => (int) $state['until'], 'delay' => (int) $state['delay'], 'error_class' => (string) $state['error_class'], 'error' => (string) $state['error'], ); } /** * Returns when a provider-wide pause ends. * * @since 1.5.0 * * @param string $provider Provider ID. * @param int|null $now Optional. Unix time. Default now. * @return int|null Unix time the pause ends, or null when not paused. */ public function get_until( string $provider, ?int $now = null ): ?int { $state = $this->get( $provider ); return null !== $state && $state['until'] > ( $now ?? time() ) ? $state['until'] : null; } /** * Checks whether a provider is paused provider-wide. * * @since 1.5.0 * * @param string $provider Provider ID. * @param int|null $now Optional. Unix time. Default now. * @return bool True when paused. */ public function is_paused( string $provider, ?int $now = null ): bool { return null !== $this->get_until( $provider, $now ); } /** * Returns when a target's pause ends. * * @since 1.5.0 * * @param \WordPress\AI\Embeddings\Sync\Embedding_Target $target The target. * @param int|null $now Optional. Unix time. Default now. * @return int|null Unix time the pause ends, or null when not paused. */ public function get_until_for( Embedding_Target $target, ?int $now = null ): ?int { $now = $now ?? time(); $until = null; foreach ( array( $this->get( $target->get_provider() ), $this->get( $target->get_provider(), $target->get_model() ) ) as $state ) { if ( null === $state || $state['until'] <= $now ) { continue; } $until = max( (int) $until, $state['until'] ); } return $until; } /** * Records a rate limit: 30 s, doubling per consecutive limit, capped at 15 min. * * @since 1.5.0 * * @param string $provider Provider ID. * @param string $error Error message. * @param int|null $now Optional. Unix time. Default now. * @return int Unix time the pause ends. */ public function record_rate_limit( string $provider, string $error, ?int $now = null ): int { $previous = $this->get( $provider ); $delay = null !== $previous && Embedding_Client_Exception::RATE_LIMITED === $previous['error_class'] ? min( self::MAX_DELAY, $previous['delay'] * 2 ) : self::MIN_DELAY; return $this->store( $provider, $delay, Embedding_Client_Exception::RATE_LIMITED, $error, $now ?? time() ); } /** * Records a provider-level failure: pause for an hour. * * @since 1.5.0 * * @param string $provider Provider ID. * @param string $error Error message, shown in sync status. * @param int|null $now Optional. Unix time. Default now. * @param string|null $model Optional. Model ID to pause only that model. Default null (provider-wide). * @return int Unix time the pause ends. */ public function record_provider_error( string $provider, string $error, ?int $now = null, ?string $model = null ): int { return $this->store( $provider, self::PROVIDER_PAUSE, Embedding_Client_Exception::PROVIDER, $error, $now ?? time(), $model ); } /** * Clears a provider's or model's state after a success or an explicit retry. * * @since 1.5.0 * * @param string $provider Provider ID. * @param string|null $model Optional. Model ID to clear model-scoped state. Default null (provider-wide). */ public function clear( string $provider, ?string $model = null ): void { if ( null === $this->get( $provider, $model ) ) { return; } delete_transient( $this->key( $provider, $model ) ); } /** * Stores a pause. * * @since 1.5.0 * * @param string $provider Provider ID. * @param int $delay Pause length in seconds. * @param string $error_class Failure class. * @param string $error Error message. * @param int $now Unix time. * @param string|null $model Optional. Model ID for model-scoped state. Default null (provider-wide). * @return int Unix time the pause ends. */ private function store( string $provider, int $delay, string $error_class, string $error, int $now, ?string $model = null ): int { $until = $now + $delay; set_transient( $this->key( $provider, $model ), array( 'until' => $until, 'delay' => $delay, 'error_class' => $error_class, // Stored raw; escaped wherever it is output. 'error' => wp_specialchars_decode( $error, ENT_QUOTES ), ), $delay + self::MAX_DELAY ); return $until; } /** * Returns the transient name for a provider, or for one of its models. * * @since 1.5.0 * * @param string $provider Provider ID. * @param string|null $model Optional. Model ID. Default null (provider-wide). * @return string The transient name. */ private function key( string $provider, ?string $model = null ): string { return self::TRANSIENT_PREFIX . md5( null === $model ? $provider : $provider . "\0" . $model ); } }