Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
RacingActionRequestStore
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 8
156
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 arm
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 transition
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 save
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 auditTrail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pendingChanges
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 accepted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace LambdaTwelve\OneRecord\Testing;
6
7use Closure;
8use LambdaTwelve\OneRecord\Api\ActionRequest;
9use LambdaTwelve\OneRecord\Api\ActionRequestType;
10use LambdaTwelve\OneRecord\Api\RequestStatus;
11use LambdaTwelve\OneRecord\Rdf\Iri;
12use LambdaTwelve\OneRecord\Server\Spi\ActionRequestStore;
13use LambdaTwelve\OneRecord\Server\Spi\AuditTrailQuery;
14use LambdaTwelve\OneRecord\Server\Spi\StoreException;
15use LambdaTwelve\OneRecord\Server\Spi\Volatile;
16
17/**
18 * Stages the race every host must prove it survives: another worker decides
19 * the same action request first. Decorates any ActionRequestStore; arm it with
20 * a request and the next transition() of that request either throws the
21 * status conflict outright, or first runs your callback (flip your own row
22 * to the competing status there) and then lets the real compare-and-set lose
23 * on its own. Fires once, then behaves like the store it wraps.
24 */
25final class RacingActionRequestStore implements ActionRequestStore, Volatile
26{
27    private ?Iri $armed = null;
28
29    /** @var ?Closure(ActionRequest): void */
30    private ?Closure $before = null;
31
32    public int $racesLost = 0;
33
34    public function __construct(private readonly ActionRequestStore $inner) {}
35
36    /**
37     * @param ?callable(ActionRequest): void $before what the competing worker did; null throws the conflict directly
38     */
39    public function arm(Iri $request, ?callable $before = null): void
40    {
41        $this->armed = $request;
42        $this->before = $before === null ? null : $before(...);
43    }
44
45    public function transition(ActionRequest $request, RequestStatus $expectedCurrent): void
46    {
47        if ($this->armed !== null && $this->armed->equals($request->iri)) {
48            $before = $this->before;
49            $this->armed = null;
50            $this->before = null;
51            $this->racesLost++;
52            if ($before === null) {
53                throw StoreException::statusConflict($request->iri, $expectedCurrent->shortName(), 'another worker decided first');
54            }
55            $before($request);
56        }
57        $this->inner->transition($request, $expectedCurrent);
58    }
59
60    public function save(ActionRequest $request): void
61    {
62        $this->inner->save($request);
63    }
64
65    public function get(Iri $iri): ?ActionRequest
66    {
67        return $this->inner->get($iri);
68    }
69
70    public function auditTrail(Iri $logisticsObject, AuditTrailQuery $query): array
71    {
72        return $this->inner->auditTrail($logisticsObject, $query);
73    }
74
75    public function pendingChanges(Iri $logisticsObject): array
76    {
77        return $this->inner->pendingChanges($logisticsObject);
78    }
79
80    public function accepted(ActionRequestType $type): array
81    {
82        return $this->inner->accepted($type);
83    }
84}