Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
DownloadProgressReporter
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
4 / 4
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 report
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 flush
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 flushAt
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service\Install;
10
11/**
12 * Coalesce frequent transfer callbacks without hiding progress from other PHP processes.
13 * The progress store remains the cross-process source of truth (cache or AppData).
14 */
15final class DownloadProgressReporter {
16    private const UPDATE_INTERVAL_SECONDS = 0.5;
17    private readonly \Closure $clock;
18    /** @var array{download_size: int, downloaded: int}|null */
19    private ?array $pending = null;
20    /** @var array{download_size: int, downloaded: int}|null */
21    private ?array $persisted = null;
22    private ?float $lastPublishedAt = null;
23
24    public function __construct(
25        private readonly InstallProgressStore $store,
26        private readonly InstallTarget $target,
27        private readonly string $resource,
28        ?\Closure $clock = null,
29    ) {
30        $this->clock = $clock ?? static fn (): float => microtime(true);
31    }
32
33    public function report(int $downloadSize, int $downloaded): void {
34        $this->pending = ['download_size' => $downloadSize, 'downloaded' => $downloaded];
35        $now = ($this->clock)();
36        if ($this->lastPublishedAt === null
37            || $now - $this->lastPublishedAt >= self::UPDATE_INTERVAL_SECONDS
38            || ($downloadSize > 0 && $downloaded >= $downloadSize)) {
39            $this->flushAt($now);
40        }
41    }
42
43    public function flush(): void {
44        $this->flushAt(($this->clock)());
45    }
46
47    private function flushAt(float $now): void {
48        if ($this->pending === null || $this->pending === $this->persisted) {
49            return;
50        }
51        // Retain metadata such as the async process PID or a previously stored error.
52        $data = $this->store->get($this->target, $this->resource);
53        $this->store->set($this->target, $this->resource, array_merge($data, $this->pending));
54        $this->persisted = $this->pending;
55        $this->lastPublishedAt = $now;
56    }
57}