sheetsClient = $sheetsClient; $this->queueRepository = $queueRepository; $this->mappingManager = $mappingManager; $this->retryPolicy = $retryPolicy; $this->logger = $logger; $this->idParser = new SpreadsheetIdParser(); } /** * Queue a normalized submission: maps fields and attempts immediate write. * Queues row if temporary errors occur. * * @param NormalizedSubmission $submission * @param array $settings Mapped settings configuration. * @return void */ public function enqueue(NormalizedSubmission $submission, array $settings): void { // 1. Map fields to cell row layout. $row = $this->mappingManager->map($submission, $settings); $connectionId = $settings['connection_id'] ?? ''; $spreadsheetId = $this->idParser->parse($settings['spreadsheet_id'] ?? ''); $worksheetName = $settings['worksheet_name'] ?? 'Sheet1'; $createHeaders = !empty($settings['create_headers']); // Hooks before sending. do_action('oneclickformsync_before_sheets_append', $row, $submission, $settings); // 2. Queue the item immediately locally. $queueItem = new QueueItem([ 'connection_id' => $connectionId, 'spreadsheet_id' => $spreadsheetId, 'worksheet_name' => $worksheetName, 'payload' => [ 'row' => $row, 'create_headers' => $createHeaders, 'header_bg_color'=> $settings['header_bg_color'] ?? '', 'header_fg_color'=> $settings['header_fg_color'] ?? '', 'header_sticky' => !empty($settings['header_sticky']), 'submission_id' => $submission->getSubmissionId(), 'form_id' => $submission->getFormId(), 'provider_id' => $submission->getProviderId(), 'submission' => $submission->toArray(), ], 'attempts' => 0, // First attempt will be handled in the background. 'status' => 'pending', 'error_message' => null, 'next_attempt_at' => current_time('mysql', true), // Ready to run immediately. ]); $this->queueRepository->save($queueItem); // Record log as queued. $this->logger->logQueued($submission, $settings, 'queued', __('Enqueued for background processing.', '1-click-form-sync')); // 3. Trigger asynchronous background process loopback immediately. $this->triggerAsyncDispatch(0); } /** * Trigger a non-blocking asynchronous loopback HTTP request to process the queue. * * @param int $delaySeconds Delay in seconds before processing. * @return void */ private function triggerAsyncDispatch(int $delaySeconds = 0): void { $url = admin_url('admin-post.php'); wp_remote_post($url, [ 'body' => [ 'action' => 'oneclickformsync_process_queue_async', 'nonce' => wp_create_nonce('oneclickformsync_process_queue_async_nonce'), 'delay' => $delaySeconds, ], 'timeout' => 0.1, // Extremely low timeout. 'blocking' => false, // Do not block execution of this process. 'sslverify' => false, ]); } /** * Process a single queued item retry. * * @param QueueItem $item * @return bool True if successful, false otherwise. */ public function processItem(QueueItem $item): bool { $connectionId = $item->getConnectionId(); $spreadsheetId = $item->getSpreadsheetId(); $worksheetName = $item->getWorksheetName(); $payloadData = $item->getPayload(); // Support both old flat arrays and new structured formats. if (isset($payloadData['row']) && is_array($payloadData['row'])) { $row = $payloadData['row']; $createHeaders = !empty($payloadData['create_headers']) && ($item->getAttempts() === 0); $headerBgColor = $payloadData['header_bg_color'] ?? ''; $headerFgColor = $payloadData['header_fg_color'] ?? ''; $headerSticky = !empty($payloadData['header_sticky']); } else { $row = $payloadData; $createHeaders = false; $headerBgColor = ''; $headerFgColor = ''; $headerSticky = false; } $item->setStatus('processing'); $this->queueRepository->save($item); // Attempt append (skips creating headers if not the first attempt). $result = $this->sheetsClient->appendRow( $connectionId, $spreadsheetId, $worksheetName, $row, $createHeaders, $headerBgColor, $headerFgColor, $headerSticky ); if ($result->isSuccess()) { $item->setStatus('completed'); $item->setErrorMessage(null); $this->queueRepository->save($item); // Log background success. $this->logger->logBackgroundSuccess($item); // Delete item from queue on success. $this->queueRepository->delete((int)$item->getId()); return true; } // Retry attempt failed. $attempts = $item->getAttempts() + 1; $item->setAttempts($attempts); $errorCode = $result->getErrorCode() ?: 'unknown_error'; $errorMessage = $result->getErrorMessage() ?: __('Background retry failed.', '1-click-form-sync'); $item->setErrorMessage($errorMessage); $isRetryable = $result->isRetryable(); $nextAttemptTime = $isRetryable ? $this->retryPolicy->getNextAttemptTimestamp($attempts) : null; if ($isRetryable && $nextAttemptTime !== null) { $item->setStatus('pending'); $item->setNextAttemptAt(gmdate('Y-m-d H:i:s', $nextAttemptTime)); $this->queueRepository->save($item); // Log background retry. $this->logger->logBackgroundQueued($item, $errorCode, $errorMessage); // Trigger background execution if delay is small (e.g. 20s for the next retry). $delaySeconds = $nextAttemptTime - current_time('timestamp', true); if ($delaySeconds > 0 && $delaySeconds <= 60) { $this->triggerAsyncDispatch($delaySeconds); } } else { // Max retries reached or error converted to permanent. Mark as failed. $item->setStatus('failed'); $item->setNextAttemptAt(null); $this->queueRepository->save($item); // Log background terminal failure. $this->logger->logBackgroundFailed($item, $errorCode, $errorMessage); } return false; } }