*/ declare(strict_types=1); namespace oat\taoQtiTest\models\runner\synchronisation\synchronisationService; use common_Exception; use common_exception_InconsistentData; use common_Logger; use oat\oatbox\service\ConfigurableService; use oat\taoQtiTest\models\runner\QtiRunnerServiceContext; use oat\taoQtiTest\models\runner\synchronisation\TestRunnerAction; use ResolverException; class ResponseGenerator extends ConfigurableService { /** * Typical amount of time added on TimePoints to avoid timestamp collisions. * This value will be used to adjust intervals between moves in the synced time line. */ private const TIMEPOINT_INTERVAL = .001; /** * @param array $data * @param array $availableActions * @return array */ public function prepareActions(array $data, array $availableActions): array { /** @var TestRunnerActionResolver $resolver */ $resolver = $this->getServiceLocator()->get(TestRunnerActionResolver::class); $actions = []; foreach ($data as $entry) { try { $actions[] = $resolver->resolve($entry, $availableActions); } catch (common_exception_InconsistentData | ResolverException $e) { $responseAction = $entry; $responseAction['error'] = $e->getMessage(); $responseAction['success'] = false; $actions[] = $this->transform($responseAction); } } // ensure the actions are in chronological order usort($actions, static function ($a, $b) { $timeA = $a instanceof TestRunnerAction ? $a->getTimestamp() : 0; $timeB = $b instanceof TestRunnerAction ? $b->getTimestamp() : 0; return $timeA - $timeB; }); return $actions; } private function transformField(string $fromFieldName, string $toFieldName, array $data): array { if (array_key_exists($fromFieldName, $data)) { $data[$toFieldName] = $data[$fromFieldName]; unset($data[$fromFieldName]); } return $data; } private function transform($data) { if (is_array($data)) { $data = $this->transformField('parameters', 'requestParameters', $data); $data = $this->transformField('action', 'name', $data); } return $data; } /** * @param array $actions * @return float */ private function computeDuration(array $actions): float { $duration = 0; foreach ($actions as $action) { if ($action instanceof TestRunnerAction && $action->hasRequestParameter('itemDuration')) { $duration += $action->getRequestParameter('itemDuration') + self::TIMEPOINT_INTERVAL; } } return $duration; } /** * determine the start timestamp of the actions: * - check if the total duration of actions to sync is comprised within * the elapsed time since the last TimePoint. * - otherwise compute the start timestamp from now minus the duration * (caution! this could introduce inconsistency in the TimeLine as the ranges could be interlaced) * * @param array $actions * @param QtiRunnerServiceContext $serviceContext * @param float $timeNow * @return float */ public function getLastActionTimestamp(array $actions, QtiRunnerServiceContext $serviceContext, float $timeNow): float { $lastRegisteredTimestamp = (float) $serviceContext->getTestSession()->getTimer()->getLastRegisteredTimestamp(); $actionsDuration = $this->computeDuration($actions); $elapsed = $timeNow - $lastRegisteredTimestamp; if ($actionsDuration > $elapsed) { common_Logger::t('Ignoring the last timestamp to take into account the actual duration to sync. Could introduce TimeLine inconsistency!'); $lastRegisteredTimestamp = $timeNow - $actionsDuration; } return $lastRegisteredTimestamp; } /** * @param TestRunnerAction $action * @param float $now * @param float $last * @param QtiRunnerServiceContext $serviceContext * @return array */ public function getActionResponse( TestRunnerAction $action, float $now, float &$last, QtiRunnerServiceContext $serviceContext ): array { try { $serviceContext->setSyncingMode($action->getRequestParameter('offline')); if ($action->hasRequestParameter('itemDuration') && $serviceContext->isSyncingMode()) { $last += $action->getRequestParameter('itemDuration') + self::TIMEPOINT_INTERVAL; $action->setTime($last); } else { $action->setTime($now); } $action->setServiceContext($serviceContext); $actionResponse = $action->process(); } catch (common_Exception $e) { $actionResponse = ['error' => $e->getMessage()]; $actionResponse['success'] = false; } $actionResponse['name'] = $action->getName(); $actionResponse['timestamp'] = $action->getTimeStamp(); $actionResponse['requestParameters'] = $action->getRequestParameters(); return $actionResponse; } }