Initial Commit

This commit is contained in:
David Stone
2024-11-30 18:24:12 -07:00
commit e8f7955c1c
5432 changed files with 1397750 additions and 0 deletions

View File

@ -0,0 +1,99 @@
<?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);
}
}

View File

@ -0,0 +1,237 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Cache;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\Amp\Serialization\SerializationException;
use WP_Ultimo\Dependencies\Amp\Sync\KeyedMutex;
use WP_Ultimo\Dependencies\Amp\Sync\Lock;
use function WP_Ultimo\Dependencies\Amp\call;
/**
* @template TValue
*/
final class AtomicCache
{
/** @var SerializedCache<TValue> */
private $cache;
/** @var KeyedMutex */
private $mutex;
/**
* @param SerializedCache<TValue> $cache
* @param KeyedMutex $mutex
*/
public function __construct(SerializedCache $cache, KeyedMutex $mutex)
{
$this->cache = $cache;
$this->mutex = $mutex;
}
/**
* Obtains the lock for the given key, then invokes the $create callback with the current cached value (which may
* be null if the key did not exist in the cache). The value returned from the callback is stored in the cache and
* the promise returned from this method is resolved with the value.
*
* @param string $key
* @param callable(string, mixed|null): mixed $create Receives $key and $value as parameters.
* @param int|null $ttl Timeout in seconds. The default `null` $ttl value indicates no timeout.
*
* @return Promise<mixed>
*
* @psalm-param callable(string, TValue|null):(TValue|Promise<TValue>|\Generator<mixed, mixed, mixed, TValue>)
* $create
* @psalm-return Promise<TValue>
*
* @throws CacheException If the $create callback throws an exception while generating the value.
* @throws SerializationException If serializing the value returned from the callback fails.
*/
public function compute(string $key, callable $create, ?int $ttl = null) : Promise
{
return call(function () use($key, $create, $ttl) : \Generator {
$lock = (yield from $this->lock($key));
\assert($lock instanceof Lock);
try {
$value = (yield $this->cache->get($key));
return yield from $this->create($create, $key, $value, $ttl);
} finally {
$lock->release();
}
});
}
/**
* Attempts to get the value for the given key. If the key is not found, the key is locked, the $create callback
* is invoked with the key as the first parameter. The value returned from the callback is stored in the cache and
* the promise returned from this method is resolved with the value.
*
* @param string $key Cache key.
* @param callable(string): mixed $create Receives $key as parameter.
* @param int|null $ttl Timeout in seconds. The default `null` $ttl value indicates no timeout.
*
* @return Promise<mixed>
*
* @psalm-param callable(string, TValue|null):(TValue|Promise<TValue>|\Generator<mixed, mixed, mixed, TValue>)
* $create
* @psalm-return Promise<TValue>
*
* @throws CacheException If the $create callback throws an exception while generating the value.
* @throws SerializationException If serializing the value returned from the callback fails.
*/
public function computeIfAbsent(string $key, callable $create, ?int $ttl = null) : Promise
{
return call(function () use($key, $create, $ttl) : \Generator {
$value = (yield $this->cache->get($key));
if ($value !== null) {
return $value;
}
$lock = (yield from $this->lock($key));
\assert($lock instanceof Lock);
try {
// Attempt to get the value again, since it may have been set while obtaining the lock.
$value = (yield $this->cache->get($key));
if ($value !== null) {
return $value;
}
return yield from $this->create($create, $key, null, $ttl);
} finally {
$lock->release();
}
});
}
/**
* Attempts to get the value for the given key. If the key exists, the key is locked, the $create callback
* is invoked with the key as the first parameter and the current key value as the second parameter. The value
* returned from the callback is stored in the cache and the promise returned from this method is resolved with
* the value.
*
* @param string $key Cache key.
* @param callable(string, mixed): mixed $create Receives $key and $value as parameters.
* @param int|null $ttl Timeout in seconds. The default `null` $ttl value indicates no timeout.
*
* @return Promise<mixed>
*
* @psalm-param callable(string, TValue|null): (TValue|Promise<TValue>|\Generator<mixed, mixed, mixed, TValue>) $create
* @psalm-return Promise<TValue>
*
* @throws CacheException If the $create callback throws an exception while generating the value.
* @throws SerializationException If serializing the value returned from the callback fails.
*/
public function computeIfPresent(string $key, callable $create, ?int $ttl = null) : Promise
{
return call(function () use($key, $create, $ttl) : \Generator {
$value = (yield $this->cache->get($key));
if ($value === null) {
return null;
}
$lock = (yield from $this->lock($key));
\assert($lock instanceof Lock);
try {
// Attempt to get the value again, since it may have been set while obtaining the lock.
$value = (yield $this->cache->get($key));
if ($value === null) {
return null;
}
return yield from $this->create($create, $key, $value, $ttl);
} finally {
$lock->release();
}
});
}
/**
* The lock is obtained for the key before setting the value.
*
* @param string $key Cache key.
* @param mixed $value Value to cache.
* @param int|null $ttl Timeout in seconds. The default `null` $ttl value indicates no timeout.
*
* @return Promise<void> Resolves either successfully or fails with a CacheException on failure.
*
* @psalm-param TValue $value
* @psalm-return Promise<void>
*
* @throws CacheException
* @throws SerializationException
*
* @see SerializedCache::set()
*/
public function set(string $key, $value, ?int $ttl = null) : Promise
{
return call(function () use($key, $value, $ttl) : \Generator {
$lock = (yield from $this->lock($key));
\assert($lock instanceof Lock);
try {
(yield $this->cache->set($key, $value, $ttl));
} finally {
$lock->release();
}
});
}
/**
* Returns the cached value for the key or the given default value if the key does not exist.
*
* @template TDefault
*
* @param string $key Cache key.
* @param mixed $default Default value returned if the key does not exist. Null by default.
*
* @return Promise<mixed|null> Resolved with null iff $default is null.
*
* @psalm-param TDefault $default
* @psalm-return Promise<TValue|TDefault>
*
* @throws CacheException
* @throws SerializationException
*
* @see SerializedCache::get()
*/
public function get(string $key, $default = null) : Promise
{
return call(function () use($key, $default) : \Generator {
$value = (yield $this->cache->get($key));
if ($value === null) {
return $default;
}
return $value;
});
}
/**
* The lock is obtained for the key before deleting the key.
*
* @param string $key
*
* @return Promise<bool|null>
*
* @see SerializedCache::delete()
*/
public function delete(string $key) : Promise
{
return call(function () use($key) : \Generator {
$lock = (yield from $this->lock($key));
\assert($lock instanceof Lock);
try {
return (yield $this->cache->delete($key));
} finally {
$lock->release();
}
});
}
private function lock(string $key) : \Generator
{
try {
return (yield $this->mutex->acquire($key));
} catch (\Throwable $exception) {
throw new CacheException(\sprintf('Exception thrown when obtaining the lock for key "%s"', $key), 0, $exception);
}
}
/**
* @psalm-param TValue|null $value
* @psalm-return \Generator<mixed, mixed, mixed, TValue>
*/
private function create(callable $create, string $key, $value, ?int $ttl) : \Generator
{
try {
$value = (yield call($create, $key, $value));
} catch (\Throwable $exception) {
throw new CacheException(\sprintf('Exception thrown while creating the value for key "%s"', $key), 0, $exception);
}
(yield $this->cache->set($key, $value, $ttl));
return $value;
}
}

48
dependencies/amphp/cache/lib/Cache.php vendored Normal file
View File

@ -0,0 +1,48 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Cache;
use WP_Ultimo\Dependencies\Amp\Promise;
interface Cache
{
/**
* Gets a value associated with the given key.
*
* If the specified key doesn't exist implementations MUST succeed the resulting promise with `null`.
*
* @param $key string Cache key.
*
* @return Promise<string|null> Resolves to the cached value nor `null` if it doesn't exist or fails with a
* CacheException on failure.
*/
public function get(string $key) : Promise;
/**
* Sets a value associated with the given key. Overrides existing values (if they exist).
*
* The eventual resolution value of the resulting promise is unimportant. The success or failure of the promise
* indicates the operation's success.
*
* @param $key string Cache key.
* @param $value string Value to cache.
* @param $ttl int Timeout in seconds. The default `null` $ttl value indicates no timeout. Values less than 0 MUST
* throw an \Error.
*
* @return Promise<void> Resolves either successfully or fails with a CacheException on failure.
*/
public function set(string $key, string $value, int $ttl = null) : Promise;
/**
* Deletes a value associated with the given key if it exists.
*
* Implementations SHOULD return boolean `true` or `false` to indicate whether the specified key existed at the time
* the delete operation was requested. If such information is not available, the implementation MUST resolve the
* promise with `null`.
*
* Implementations MUST transparently succeed operations for non-existent keys.
*
* @param $key string Cache key.
*
* @return Promise<bool|null> Resolves to `true` / `false` to indicate whether the key existed or fails with a
* CacheException on failure. May also resolve with `null` if that information is not available.
*/
public function delete(string $key) : Promise;
}

View File

@ -0,0 +1,10 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Cache;
/**
* MUST be thrown in case a cache operation fails.
*/
class CacheException extends \Exception
{
}

View File

@ -0,0 +1,147 @@
<?php
/** @noinspection PhpUndefinedFunctionInspection */
namespace WP_Ultimo\Dependencies\Amp\Cache;
use WP_Ultimo\Dependencies\Amp\File;
use WP_Ultimo\Dependencies\Amp\File\Driver;
use WP_Ultimo\Dependencies\Amp\Loop;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\Amp\Sync\KeyedMutex;
use WP_Ultimo\Dependencies\Amp\Sync\Lock;
use function WP_Ultimo\Dependencies\Amp\call;
final class FileCache implements Cache
{
private static function getFilename(string $key) : string
{
return \hash('sha256', $key) . '.cache';
}
/** @var string */
private $directory;
/** @var KeyedMutex */
private $mutex;
/** @var string */
private $gcWatcher;
/** @var bool */
private $ampFileVersion2;
public function __construct(string $directory, KeyedMutex $mutex)
{
$this->directory = $directory = \rtrim($directory, "/\\");
$this->mutex = $mutex;
if (!\interface_exists(Driver::class)) {
throw new \Error(__CLASS__ . ' requires amphp/file to be installed');
}
$this->ampFileVersion2 = $ampFileVersion2 = \function_exists('WP_Ultimo\\Dependencies\\Amp\\File\\listFiles');
$gcWatcher = static function () use($directory, $mutex, $ampFileVersion2) : \Generator {
try {
/** @psalm-suppress UndefinedFunction */
$files = (yield $ampFileVersion2 ? File\listFiles($directory) : File\scandir($directory));
foreach ($files as $file) {
if (\strlen($file) !== 70 || \substr($file, -\strlen('.cache')) !== '.cache') {
continue;
}
/** @var Lock $lock */
$lock = (yield $mutex->acquire($file));
try {
/** @var File\File $handle */
/** @psalm-suppress UndefinedFunction */
$handle = (yield $ampFileVersion2 ? File\openFile($directory . '/' . $file, 'r') : File\open($directory . '/' . $file, 'r'));
$ttl = (yield $handle->read(4));
if ($ttl === null || \strlen($ttl) !== 4) {
(yield $handle->close());
continue;
}
$ttl = \unpack('Nttl', $ttl)['ttl'];
if ($ttl < \time()) {
/** @psalm-suppress UndefinedFunction */
(yield $ampFileVersion2 ? File\deleteFile($directory . '/' . $file) : File\unlink($directory . '/' . $file));
}
} catch (\Throwable $e) {
// ignore
} finally {
$lock->release();
}
}
} catch (\Throwable $e) {
// ignore
}
};
// trigger once, so short running scripts also GC and don't grow forever
Loop::defer($gcWatcher);
$this->gcWatcher = Loop::repeat(300000, $gcWatcher);
}
public function __destruct()
{
if ($this->gcWatcher !== null) {
Loop::cancel($this->gcWatcher);
}
}
/** @inheritdoc */
public function get(string $key) : Promise
{
return call(function () use($key) {
$filename = $this->getFilename($key);
/** @var Lock $lock */
$lock = (yield $this->mutex->acquire($filename));
try {
/** @psalm-suppress UndefinedFunction */
$cacheContent = (yield $this->ampFileVersion2 ? File\read($this->directory . '/' . $filename) : File\get($this->directory . '/' . $filename));
if (\strlen($cacheContent) < 4) {
return null;
}
$ttl = \unpack('Nttl', \substr($cacheContent, 0, 4))['ttl'];
if ($ttl < \time()) {
/** @psalm-suppress UndefinedFunction */
(yield $this->ampFileVersion2 ? File\deleteFile($this->directory . '/' . $filename) : File\unlink($this->directory . '/' . $filename));
return null;
}
$value = \substr($cacheContent, 4);
\assert(\is_string($value));
return $value;
} catch (\Throwable $e) {
return null;
} finally {
$lock->release();
}
});
}
/** @inheritdoc */
public function set(string $key, string $value, int $ttl = null) : Promise
{
if ($ttl < 0) {
throw new \Error("Invalid cache TTL ({$ttl}); integer >= 0 or null required");
}
return call(function () use($key, $value, $ttl) {
$filename = $this->getFilename($key);
/** @var Lock $lock */
$lock = (yield $this->mutex->acquire($filename));
if ($ttl === null) {
$ttl = \PHP_INT_MAX;
} else {
$ttl = \time() + $ttl;
}
$encodedTtl = \pack('N', $ttl);
try {
/** @psalm-suppress UndefinedFunction */
(yield $this->ampFileVersion2 ? File\write($this->directory . '/' . $filename, $encodedTtl . $value) : File\put($this->directory . '/' . $filename, $encodedTtl . $value));
} finally {
$lock->release();
}
});
}
/** @inheritdoc */
public function delete(string $key) : Promise
{
return call(function () use($key) {
$filename = $this->getFilename($key);
/** @var Lock $lock */
$lock = (yield $this->mutex->acquire($filename));
try {
/** @psalm-suppress UndefinedFunction */
return (yield $this->ampFileVersion2 ? File\deleteFile($this->directory . '/' . $filename) : File\unlink($this->directory . '/' . $filename));
} finally {
$lock->release();
}
});
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Cache;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\Amp\Success;
/**
* Cache implementation that just ignores all operations and always resolves to `null`.
*/
class NullCache implements Cache
{
/** @inheritdoc */
public function get(string $key) : Promise
{
return new Success();
}
/** @inheritdoc */
public function set(string $key, string $value, int $ttl = null) : Promise
{
/** @var Promise<void> */
return new Success();
}
/** @inheritdoc */
public function delete(string $key) : Promise
{
return new Success(\false);
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Cache;
use WP_Ultimo\Dependencies\Amp\Promise;
final class PrefixCache implements Cache
{
private $cache;
private $keyPrefix;
public function __construct(Cache $cache, string $keyPrefix)
{
$this->cache = $cache;
$this->keyPrefix = $keyPrefix;
}
/**
* Gets the specified key prefix.
*
* @return string
*/
public function getKeyPrefix() : string
{
return $this->keyPrefix;
}
/** @inheritdoc */
public function get(string $key) : Promise
{
return $this->cache->get($this->keyPrefix . $key);
}
/** @inheritdoc */
public function set(string $key, string $value, int $ttl = null) : Promise
{
return $this->cache->set($this->keyPrefix . $key, $value, $ttl);
}
/** @inheritdoc */
public function delete(string $key) : Promise
{
return $this->cache->delete($this->keyPrefix . $key);
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace WP_Ultimo\Dependencies\Amp\Cache;
use WP_Ultimo\Dependencies\Amp\Failure;
use WP_Ultimo\Dependencies\Amp\Promise;
use WP_Ultimo\Dependencies\Amp\Serialization\SerializationException;
use WP_Ultimo\Dependencies\Amp\Serialization\Serializer;
use function WP_Ultimo\Dependencies\Amp\call;
/**
* @template TValue
*/
final class SerializedCache
{
/** @var Cache */
private $cache;
/** @var Serializer */
private $serializer;
public function __construct(Cache $cache, Serializer $serializer)
{
$this->cache = $cache;
$this->serializer = $serializer;
}
/**
* Fetch a value from the cache and unserialize it.
*
* @param $key string Cache key.
*
* @return Promise<mixed|null> Resolves to the cached value or `null` if it doesn't exist. Fails with a
* CacheException or SerializationException on failure.
*
* @psalm-return Promise<TValue|null>
*
* @see Cache::get()
*/
public function get(string $key) : Promise
{
return call(function () use($key) {
$data = (yield $this->cache->get($key));
if ($data === null) {
return null;
}
return $this->serializer->unserialize($data);
});
}
/**
* Serializes a value and stores its serialization to the cache.
*
* @param $key string Cache key.
* @param $value mixed Value to cache.
* @param $ttl int Timeout in seconds. The default `null` $ttl value indicates no timeout. Values less than 0 MUST
* throw an \Error.
*
* @psalm-param TValue $value
*
* @return Promise<void> Resolves either successfully or fails with a CacheException or SerializationException.
*
* @see Cache::set()
*/
public function set(string $key, $value, int $ttl = null) : Promise
{
if ($value === null) {
return new Failure(new CacheException('Cannot store NULL in serialized cache'));
}
try {
$value = $this->serializer->serialize($value);
} catch (SerializationException $exception) {
return new Failure($exception);
}
return $this->cache->set($key, $value, $ttl);
}
/**
* Deletes a value associated with the given key if it exists.
*
* @param $key string Cache key.
*
* @return Promise<bool|null> Resolves to `true` / `false` to indicate whether the key existed or fails with a
* CacheException on failure. May also resolve with `null` if that information is not available.
*
* @see Cache::delete()
*/
public function delete(string $key) : Promise
{
return $this->cache->delete($key);
}
}