- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
209 lines
5.5 KiB
PHP
209 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Ksuid;
|
|
|
|
use App\Framework\Random\RandomGenerator;
|
|
use DateTimeImmutable;
|
|
use InvalidArgumentException;
|
|
|
|
/**
|
|
* KSUID Generator Service
|
|
*
|
|
* Generates K-Sortable Unique Identifiers with timestamp ordering.
|
|
*/
|
|
final readonly class KsuidGenerator
|
|
{
|
|
public function __construct(
|
|
private RandomGenerator $randomGenerator
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Generate a new KSUID with current timestamp
|
|
*/
|
|
public function generate(): Ksuid
|
|
{
|
|
return $this->generateAt(time());
|
|
}
|
|
|
|
/**
|
|
* Generate a KSUID at a specific Unix timestamp
|
|
*/
|
|
public function generateAt(int $timestamp): Ksuid
|
|
{
|
|
if ($timestamp < Ksuid::EPOCH) {
|
|
throw new InvalidArgumentException('Timestamp cannot be before KSUID epoch (2014-05-13)');
|
|
}
|
|
|
|
// Generate 16 random bytes for payload
|
|
$payload = $this->randomGenerator->bytes(Ksuid::PAYLOAD_BYTES);
|
|
|
|
return Ksuid::fromTimestampAndPayload($timestamp, $payload);
|
|
}
|
|
|
|
/**
|
|
* Generate a KSUID at a specific DateTime
|
|
*/
|
|
public function generateAtDateTime(DateTimeImmutable $dateTime): Ksuid
|
|
{
|
|
return $this->generateAt($dateTime->getTimestamp());
|
|
}
|
|
|
|
/**
|
|
* Generate a KSUID for the past (useful for testing)
|
|
*/
|
|
public function generateInPast(int $secondsAgo): Ksuid
|
|
{
|
|
$timestamp = time() - $secondsAgo;
|
|
|
|
return $this->generateAt($timestamp);
|
|
}
|
|
|
|
/**
|
|
* Generate a batch of KSUIDs with the same timestamp
|
|
*/
|
|
public function generateBatch(int $count, ?int $timestamp = null): array
|
|
{
|
|
if ($count <= 0) {
|
|
throw new InvalidArgumentException('Count must be positive');
|
|
}
|
|
|
|
if ($count > 10000) {
|
|
throw new InvalidArgumentException('Batch size cannot exceed 10000');
|
|
}
|
|
|
|
$timestamp ??= time();
|
|
$ksuids = [];
|
|
$used = [];
|
|
|
|
while (count($ksuids) < $count) {
|
|
$payload = $this->randomGenerator->bytes(Ksuid::PAYLOAD_BYTES);
|
|
$payloadHex = bin2hex($payload);
|
|
|
|
// Ensure uniqueness within batch
|
|
if (! isset($used[$payloadHex])) {
|
|
$ksuids[] = Ksuid::fromTimestampAndPayload($timestamp, $payload);
|
|
$used[$payloadHex] = true;
|
|
}
|
|
}
|
|
|
|
return $ksuids;
|
|
}
|
|
|
|
/**
|
|
* Generate a sequence of KSUIDs with incrementing timestamps
|
|
*/
|
|
public function generateSequence(int $count, int $intervalSeconds = 1): array
|
|
{
|
|
if ($count <= 0) {
|
|
throw new InvalidArgumentException('Count must be positive');
|
|
}
|
|
|
|
if ($count > 1000) {
|
|
throw new InvalidArgumentException('Sequence size cannot exceed 1000');
|
|
}
|
|
|
|
if ($intervalSeconds < 0) {
|
|
throw new InvalidArgumentException('Interval must be non-negative');
|
|
}
|
|
|
|
$ksuids = [];
|
|
$timestamp = time();
|
|
|
|
for ($i = 0; $i < $count; $i++) {
|
|
$ksuids[] = $this->generateAt($timestamp + ($i * $intervalSeconds));
|
|
}
|
|
|
|
return $ksuids;
|
|
}
|
|
|
|
/**
|
|
* Generate a KSUID with specific prefix in payload (for testing/debugging)
|
|
*/
|
|
public function generateWithPrefix(string $prefix): Ksuid
|
|
{
|
|
$maxPrefixLength = Ksuid::PAYLOAD_BYTES - 1; // Reserve at least 1 byte for randomness
|
|
|
|
if (strlen($prefix) > $maxPrefixLength) {
|
|
throw new InvalidArgumentException("Prefix cannot exceed {$maxPrefixLength} bytes");
|
|
}
|
|
|
|
$remainingBytes = Ksuid::PAYLOAD_BYTES - strlen($prefix);
|
|
$randomSuffix = $this->randomGenerator->bytes($remainingBytes);
|
|
$payload = $prefix . $randomSuffix;
|
|
|
|
return Ksuid::fromTimestampAndPayload(time(), $payload);
|
|
}
|
|
|
|
/**
|
|
* Parse a KSUID string and validate it
|
|
*/
|
|
public function parse(string $ksuidString): Ksuid
|
|
{
|
|
return Ksuid::fromString($ksuidString);
|
|
}
|
|
|
|
/**
|
|
* Validate if a string is a valid KSUID
|
|
*/
|
|
public function isValid(string $value): bool
|
|
{
|
|
if (strlen($value) !== Ksuid::ENCODED_LENGTH) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
Ksuid::fromString($value);
|
|
|
|
return true;
|
|
} catch (InvalidArgumentException) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the minimum possible KSUID for a timestamp
|
|
*/
|
|
public function getMinForTimestamp(int $timestamp): Ksuid
|
|
{
|
|
$payload = str_repeat("\0", Ksuid::PAYLOAD_BYTES);
|
|
|
|
return Ksuid::fromTimestampAndPayload($timestamp, $payload);
|
|
}
|
|
|
|
/**
|
|
* Get the maximum possible KSUID for a timestamp
|
|
*/
|
|
public function getMaxForTimestamp(int $timestamp): Ksuid
|
|
{
|
|
$payload = str_repeat("\xFF", Ksuid::PAYLOAD_BYTES);
|
|
|
|
return Ksuid::fromTimestampAndPayload($timestamp, $payload);
|
|
}
|
|
|
|
/**@return array{min: \App\Framework\Ksuid\Ksuid, max: \App\Framework\Ksuid\Ksuid}
|
|
* Generate KSUIDs for a time range (useful for queries)
|
|
*/
|
|
public function generateTimeRange(int $startTimestamp, int $endTimestamp): array
|
|
{
|
|
if ($startTimestamp >= $endTimestamp) {
|
|
throw new InvalidArgumentException('Start timestamp must be before end timestamp');
|
|
}
|
|
|
|
return [
|
|
'min' => $this->getMinForTimestamp($startTimestamp),
|
|
'max' => $this->getMaxForTimestamp($endTimestamp),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Create factory method
|
|
*/
|
|
public static function create(RandomGenerator $randomGenerator): self
|
|
{
|
|
return new self($randomGenerator);
|
|
}
|
|
}
|