|
21 | 21 | use Windwalker\Cache\Storage\GroupedStorageInterface; |
22 | 22 | use Windwalker\Cache\Storage\PhpFileStorage; |
23 | 23 | use Windwalker\Cache\Storage\StorageInterface; |
| 24 | +use Windwalker\Promise\Promise; |
24 | 25 | use Windwalker\Utilities\Assert\ArgumentsAssert; |
25 | 26 |
|
26 | 27 | /** |
@@ -279,16 +280,13 @@ public function get($key, $default = null): mixed |
279 | 280 | /** |
280 | 281 | * @inheritDoc |
281 | 282 | */ |
282 | | - public function set($key, $value, $ttl = null, array $tags = []): bool |
| 283 | + public function set($key, $value, $ttl = null): bool |
283 | 284 | { |
284 | 285 | $item = $this->getItem($key); |
285 | 286 |
|
286 | 287 | $item->expiresAfter($ttl ?? $this->defaultTtl); |
287 | 288 | $item->set($value); |
288 | 289 |
|
289 | | - if ($item instanceof CacheItem && $tags !== []) { |
290 | | - $item->tags(...$tags); |
291 | | - } |
292 | 290 |
|
293 | 291 | return $this->save($item); |
294 | 292 | } |
@@ -367,7 +365,6 @@ public function has($key): bool |
367 | 365 | * @param string $key |
368 | 366 | * @param CacheHandler $handler Invoked to compute the value on cache miss. |
369 | 367 | * Receives the CacheItem as first argument. |
370 | | - * May call $item->tag('foo', 'bar') to associate tags. |
371 | 368 | * @param null|int|DateInterval $ttl |
372 | 369 | * @param float $beta XFetch beta factor. |
373 | 370 | * 0 = no early expiration. |
@@ -451,6 +448,108 @@ public function call( |
451 | 448 | return $this->fetch($key, $handler, $ttl, 1.0, $lock); |
452 | 449 | } |
453 | 450 |
|
| 451 | + /** |
| 452 | + * Asynchronous variant of fetch() that wraps the computation in a Promise. |
| 453 | + * |
| 454 | + * Differences from fetch(): |
| 455 | + * - The handler may return either a value OR a Promise / thenable. |
| 456 | + * When a thenable is returned, the cache will only persist the resolved value |
| 457 | + * (NOT the promise object itself). |
| 458 | + * - The returned Promise can be combined with Promise::all(), Promise::race(), etc. |
| 459 | + * |
| 460 | + * Example: |
| 461 | + * Promise::all([ |
| 462 | + * $pool->fetchAsync('a', fn() => fetchRemote('a')), // handler returns Promise |
| 463 | + * $pool->fetchAsync('b', fn() => fetchRemote('b')), |
| 464 | + * ])->then(fn(array $values) => useValues($values))->wait(); |
| 465 | + * |
| 466 | + * @psalm-param callable(CacheItem): mixed $handler |
| 467 | + */ |
| 468 | + public function fetchAsync( |
| 469 | + string $key, |
| 470 | + callable $handler, |
| 471 | + DateInterval|int|null $ttl = null, |
| 472 | + float $beta = 1.0, |
| 473 | + bool $lock = true, |
| 474 | + ): Promise { |
| 475 | + if (!class_exists(Promise::class)) { |
| 476 | + throw new \DomainException('Please install `windwalker/promise` to use fetchAsync().'); |
| 477 | + } |
| 478 | + |
| 479 | + return new Promise(function ($resolve, $reject) use ($key, $handler, $ttl, $beta, $lock) { |
| 480 | + $locked = $lock && CacheLock::lock($key, $isNew); |
| 481 | + $released = false; |
| 482 | + |
| 483 | + $release = static function () use (&$released, &$locked, $key) { |
| 484 | + if ($locked && !$released) { |
| 485 | + CacheLock::release($key); |
| 486 | + $released = true; |
| 487 | + } |
| 488 | + }; |
| 489 | + |
| 490 | + try { |
| 491 | + $item = $this->getItem($key); |
| 492 | + |
| 493 | + // Re-entrant: another stripe in this process already holds the lock. |
| 494 | + if ($locked && !$isNew) { |
| 495 | + $release(); |
| 496 | + $resolve($item->get()); |
| 497 | + |
| 498 | + return; |
| 499 | + } |
| 500 | + |
| 501 | + $isHit = $item->isHit(); |
| 502 | + |
| 503 | + if ($isHit && !$this->shouldRecomputeEarly($item, $beta)) { |
| 504 | + $release(); |
| 505 | + $resolve($item->get()); |
| 506 | + |
| 507 | + return; |
| 508 | + } |
| 509 | + |
| 510 | + $item->expiresAfter($ttl); |
| 511 | + |
| 512 | + $start = microtime(true); |
| 513 | + $data = $handler($item); |
| 514 | + |
| 515 | + // Whether $data is a plain value or a Promise/thenable, Promise::resolve() |
| 516 | + // normalises it. The .then() chain only fires once the (possibly async) |
| 517 | + // value has actually been produced. |
| 518 | + Promise::resolve($data)->then( |
| 519 | + function ($resolvedData) use ($item, $start, $resolve, $release) { |
| 520 | + try { |
| 521 | + $ctime = max(1, (int) round(max(0.0, microtime(true) - $start) * 1000)); |
| 522 | + |
| 523 | + if (!$resolvedData instanceof CacheItemInterface) { |
| 524 | + $item->set($resolvedData); |
| 525 | + } else { |
| 526 | + $item = $resolvedData; |
| 527 | + $resolvedData = $item->get(); |
| 528 | + } |
| 529 | + |
| 530 | + if ($item instanceof CacheItem) { |
| 531 | + $item->setCtime($ctime); |
| 532 | + } |
| 533 | + |
| 534 | + $this->save($item); |
| 535 | + } finally { |
| 536 | + $release(); |
| 537 | + } |
| 538 | + |
| 539 | + $resolve($resolvedData); |
| 540 | + }, |
| 541 | + function ($reason) use ($release, $reject) { |
| 542 | + $release(); |
| 543 | + $reject($reason); |
| 544 | + } |
| 545 | + ); |
| 546 | + } catch (\Throwable $e) { |
| 547 | + $release(); |
| 548 | + $reject($e); |
| 549 | + } |
| 550 | + }); |
| 551 | + } |
| 552 | + |
454 | 553 | /** |
455 | 554 | * XFetch probabilistic early-expiration check. |
456 | 555 | * |
|
0 commit comments