<?php namespace WP_Ultimo\Dependencies\Amp\Cache; use WP_Ultimo\Dependencies\Amp\Loop; use WP_Ultimo\Dependencies\Amp\Promise; use WP_Ultimo\Dependencies\Amp\Struct; use WP_Ultimo\Dependencies\Amp\Success; final class ArrayCache implements Cache { /** @var object */ private $sharedState; /** @var string */ private $ttlWatcherId; /** @var int|null */ private $maxSize; /** * @param int $gcInterval The frequency in milliseconds at which expired cache entries should be garbage collected. * @param int $maxSize The maximum size of cache array (number of elements). */ public function __construct(int $gcInterval = 5000, int $maxSize = null) { // By using a shared state object we're able to use `__destruct()` for "normal" garbage collection of both this // instance and the loop's watcher. Otherwise this object could only be GC'd when the TTL watcher was cancelled // at the loop layer. $this->sharedState = $sharedState = new class { use Struct; /** @var string[] */ public $cache = []; /** @var int[] */ public $cacheTimeouts = []; /** @var bool */ public $isSortNeeded = \false; public function collectGarbage() : void { $now = \time(); if ($this->isSortNeeded) { \asort($this->cacheTimeouts); $this->isSortNeeded = \false; } foreach ($this->cacheTimeouts as $key => $expiry) { if ($now <= $expiry) { break; } unset($this->cache[$key], $this->cacheTimeouts[$key]); } } }; $this->ttlWatcherId = Loop::repeat($gcInterval, [$sharedState, "collectGarbage"]); $this->maxSize = $maxSize; Loop::unreference($this->ttlWatcherId); } public function __destruct() { $this->sharedState->cache = []; $this->sharedState->cacheTimeouts = []; Loop::cancel($this->ttlWatcherId); } /** @inheritdoc */ public function get(string $key) : Promise { if (!isset($this->sharedState->cache[$key])) { return new Success(null); } if (isset($this->sharedState->cacheTimeouts[$key]) && \time() > $this->sharedState->cacheTimeouts[$key]) { unset($this->sharedState->cache[$key], $this->sharedState->cacheTimeouts[$key]); return new Success(null); } return new Success($this->sharedState->cache[$key]); } /** @inheritdoc */ public function set(string $key, string $value, int $ttl = null) : Promise { if ($ttl === null) { unset($this->sharedState->cacheTimeouts[$key]); } elseif ($ttl >= 0) { $expiry = \time() + $ttl; $this->sharedState->cacheTimeouts[$key] = $expiry; $this->sharedState->isSortNeeded = \true; } else { throw new \Error("Invalid cache TTL ({$ttl}; integer >= 0 or null required"); } unset($this->sharedState->cache[$key]); if (\count($this->sharedState->cache) === $this->maxSize) { \array_shift($this->sharedState->cache); } $this->sharedState->cache[$key] = $value; /** @var Promise<void> */ return new Success(); } /** @inheritdoc */ public function delete(string $key) : Promise { $exists = isset($this->sharedState->cache[$key]); unset($this->sharedState->cache[$key], $this->sharedState->cacheTimeouts[$key]); return new Success($exists); } }