Skip to content

Commit 81e9e03

Browse files
committed
fix: address review feedback on RequestQueueClient typing refactor
- batchDeleteRequests() now rejects an empty or over-25-request input up front, matching batchAddRequests() and the JS reference. - Added RequestQueueRequest::getRetryCount()/getLockExpiresAt(), so listAndLockHead()'s per-item lock-expiry field is actually reachable via a typed getter as the LockedRequestQueueHead docblock claimed. - Fixed the getLimit() return-type docs for the two new models.
1 parent 69b3a0f commit 81e9e03

7 files changed

Lines changed: 84 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ OpenAPI-documented response schemas and the reference client's typed result inte
1515
- `RequestQueueHead` and the new `LockedRequestQueueHead` gained the previously-missing
1616
`getQueueModifiedAt()` getter (the field is present in the OpenAPI spec and the reference client,
1717
but was not yet exposed by this client).
18+
- `RequestQueueRequest` gained `getRetryCount()`/`getLockExpiresAt()` getters, populated on requests
19+
returned by `listHead()`/`listAndLockHead()`/`listRequests()`.
20+
- `batchDeleteRequests()` now throws `InvalidArgumentException` up front for an empty or
21+
over-25-request input, matching `batchAddRequests()`'s and the reference client's validation.
1822

1923
## 0.4.0
2024

docs/models.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ Returned by `listAndLockHead()`.
183183
| Getter | Description |
184184
|---|---|
185185
| `getItems(): array` | The locked `RequestQueueRequest` items at the head of the queue. |
186-
| `getLimit(): ?int` | The requested head size limit. |
186+
| `getLimit(): int` | The requested head size limit. |
187187
| `hadMultipleClients(): bool` | Whether multiple clients have accessed the queue. |
188188
| `getLockSecs(): int` | The lock duration applied to every returned request. |
189189
| `queueHasLockedRequests(): ?bool` | Whether the queue has any requests locked by any client. |
@@ -195,7 +195,7 @@ Returned by `listRequests()`.
195195
| Getter | Description |
196196
|---|---|
197197
| `getItems(): array` | The `RequestQueueRequest` items in this page. |
198-
| `getLimit(): ?int` | The requested page size limit. |
198+
| `getLimit(): int` | The requested page size limit. |
199199
| `getExclusiveStartId(): ?string` | The exclusive start ID used for this page (deprecated; use the cursor). |
200200
| `getCursor(): ?string` | The cursor that produced this page. |
201201
| `getNextCursor(): ?string` | The cursor to request the next page, or `null` if this is the last page. |
@@ -264,6 +264,8 @@ Pass `url` and `uniqueKey` positionally for the common case; `data` seeds any ad
264264
| `getUniqueKey(): ?string` / `setUniqueKey(string): self` | The deduplication key. |
265265
| `getMethod(): ?string` / `setMethod(string): self` | The HTTP method (defaults to `GET`). |
266266
| `getUserData(): mixed` / `setUserData(array): self` | Arbitrary user data attached to the request. |
267+
| `getRetryCount(): ?int` | How many times this request has already been retried (read-only, populated by `listHead()`/`listAndLockHead()`/`listRequests()`). |
268+
| `getLockExpiresAt(): ?string` | When this request's lock expires (read-only, populated by `listAndLockHead()` only). |
267269

268270
### `ActorEnvVar`
269271
Constructor: `new ActorEnvVar(?string $name = null, ?string $value = null, ?bool $isSecret = null, array $data = [])`.

docs/storages.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Single — `$client->requestQueue($id)`:
9191
- `addRequest(RequestQueueRequest $request, bool $forefront = false): RequestQueueOperationInfo` — adds a request to the queue; when `$forefront` is `true` it is added to the front (handled before the rest) instead of the back.
9292
- `getRequest(string $id): ?RequestQueueRequest`, `updateRequest(RequestQueueRequest $request, bool $forefront = false): RequestQueueOperationInfo` (with `$forefront` `true` the updated request is moved to the front of the queue), `deleteRequest(string $id): void`
9393
- `batchAddRequests(array $requests, bool $forefront = false, ?BatchAddRequestsOptions $options = null): BatchAddResult` — every request must have a non-empty `uniqueKey`; with `$forefront` `true` the requests are added to the front of the queue; input is split into batches of at most 25 requests that also respect the ~9 MiB payload limit.
94-
- `batchDeleteRequests(array $requests): BatchDeleteResult``$requests` is a `list<RequestQueueRequest>` where each entry identifies a request to delete via `setId()` or `setUniqueKey()` (other fields, if set, are ignored by the API).
94+
- `batchDeleteRequests(array $requests): BatchDeleteResult``$requests` is a `list<RequestQueueRequest>` (1-25 entries) where each entry identifies a request to delete via `setId()` or `setUniqueKey()` (other fields, if set, are ignored by the API); throws `InvalidArgumentException` if empty or over 25 (unlike `batchAddRequests()`, oversized input is rejected rather than chunked, since delete is idempotent and can simply be called again).
9595
- `listRequests(?ListRequestsOptions $options = null): RequestQueueRequestsPage`
9696
- `paginateRequests(?PaginateRequestsOptions $options = null): iterable` — lazily iterate the queue's requests, yielding `RequestQueueRequest` instances and following cursor pagination (see the options note below).
9797
- `listAndLockHead(int $lockSecs, ?int $limit = null): LockedRequestQueueHead` — atomically returns and locks up to `$limit` requests for `$lockSecs` seconds.

src/Model/LockedRequestQueueHead.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ public static function fromData(mixed $data): self
4848
}
4949

5050
/**
51-
* The locked requests from the head of the queue. Each item carries its own
52-
* {@see RequestQueueRequest} lock-expiry field as reported by the API.
51+
* The locked requests from the head of the queue. Each item's own
52+
* {@see RequestQueueRequest::getLockExpiresAt()} reports when its individual lock expires.
5353
*
5454
* @return list<RequestQueueRequest>
5555
*/

src/Model/RequestQueueRequest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,25 @@ public function setUserData(mixed $userData): self
9595
$this->data['userData'] = $userData;
9696
return $this;
9797
}
98+
99+
/**
100+
* How many times processing this request has already been retried. Populated on requests
101+
* returned by {@see \Apify\Client\Resource\RequestQueueClient::listHead()},
102+
* {@see \Apify\Client\Resource\RequestQueueClient::listAndLockHead()} and
103+
* {@see \Apify\Client\Resource\RequestQueueClient::listRequests()}; absent when constructing a
104+
* request to add.
105+
*/
106+
public function getRetryCount(): ?int
107+
{
108+
return $this->getInt('retryCount');
109+
}
110+
111+
/**
112+
* ISO 8601 timestamp of when this request's processing lock expires. Only present on requests
113+
* returned by {@see \Apify\Client\Resource\RequestQueueClient::listAndLockHead()}.
114+
*/
115+
public function getLockExpiresAt(): ?string
116+
{
117+
return $this->getString('lockExpiresAt');
118+
}
98119
}

src/Resource/RequestQueueClient.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,12 +398,30 @@ private static function sleepBackoff(int $attempt, int $minDelayMillis): void
398398
* either {@see RequestQueueRequest::setId()} or {@see RequestQueueRequest::setUniqueKey()} (other
399399
* fields, if present, are ignored by the API).
400400
*
401+
* Unlike {@see batchAddRequests()}, this does not chunk oversized input: the API caps a single
402+
* batch at 25 requests (matching the reference client), so a larger input is rejected up front
403+
* rather than silently split (a delete is idempotent, so callers can simply call this again per
404+
* chunk).
405+
*
401406
* @param list<RequestQueueRequest> $requests
407+
* @throws InvalidArgumentException if {@code $requests} is empty or exceeds the per-call limit
402408
*/
403409
public function batchDeleteRequests(array $requests): BatchDeleteResult
404410
{
411+
$requests = array_values($requests);
412+
if ($requests === []) {
413+
throw new InvalidArgumentException('batchDeleteRequests: $requests must not be empty');
414+
}
415+
if (count($requests) > self::MAX_REQUESTS_PER_BATCH) {
416+
throw new InvalidArgumentException(sprintf(
417+
'batchDeleteRequests: got %d requests, which exceeds the maximum of %d per call',
418+
count($requests),
419+
self::MAX_REQUESTS_PER_BATCH
420+
));
421+
}
422+
405423
$params = $this->applyClientKey(new QueryParams());
406-
$payload = array_map(static fn (RequestQueueRequest $r) => $r->toArray(), array_values($requests));
424+
$payload = array_map(static fn (RequestQueueRequest $r) => $r->toArray(), $requests);
407425
return BatchDeleteResult::fromData($this->ctx->deleteWithBody('requests/batch', $params, $payload));
408426
}
409427

tests/Unit/RequestQueueTypedResultsTest.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Apify\Client\ApifyClient;
88
use Apify\Client\Internal\Json;
99
use Apify\Client\Model\RequestQueueRequest;
10+
use InvalidArgumentException;
1011
use PHPUnit\Framework\TestCase;
1112

1213
/**
@@ -47,14 +48,16 @@ public function testListAndLockHeadReturnsTypedResult(): void
4748
'queueHasLockedRequests' => true,
4849
'clientKey' => 'my-client-key',
4950
'items' => [
50-
['id' => 'r1', 'uniqueKey' => 'r1', 'url' => 'https://a.com'],
51+
['id' => 'r1', 'uniqueKey' => 'r1', 'url' => 'https://a.com', 'retryCount' => 2, 'lockExpiresAt' => '2026-08-01T00:01:00.000Z'],
5152
],
5253
]]));
5354

5455
$locked = $this->client($transport)->requestQueue('q1')->listAndLockHead(60, 5);
5556

5657
self::assertCount(1, $locked->getItems());
5758
self::assertSame('r1', $locked->getItems()[0]->getId());
59+
self::assertSame(2, $locked->getItems()[0]->getRetryCount());
60+
self::assertSame('2026-08-01T00:01:00.000Z', $locked->getItems()[0]->getLockExpiresAt());
5861
self::assertSame(60, $locked->getLockSecs());
5962
self::assertTrue($locked->queueHasLockedRequests());
6063
self::assertSame('my-client-key', $locked->getClientKey());
@@ -125,4 +128,33 @@ public function testBatchDeleteRequestsSendsIdentifiersAndReturnsTypedResult():
125128
$sentBody = Json::decode(MockTransport::readBody($transport->lastRequest()));
126129
self::assertSame([['id' => 'r1'], ['id' => 'r2']], $sentBody);
127130
}
131+
132+
public function testBatchDeleteRequestsRejectsEmptyInputBeforeAnyCall(): void
133+
{
134+
$transport = new MockTransport();
135+
136+
$this->expectException(InvalidArgumentException::class);
137+
try {
138+
$this->client($transport)->requestQueue('q1')->batchDeleteRequests([]);
139+
} finally {
140+
self::assertSame(0, $transport->callCount());
141+
}
142+
}
143+
144+
public function testBatchDeleteRequestsRejectsOversizedInputBeforeAnyCall(): void
145+
{
146+
$transport = new MockTransport();
147+
$requests = array_map(
148+
static fn (int $i) => (new RequestQueueRequest())->setId('r' . $i),
149+
range(0, 25) // 26 > the 25-per-call limit
150+
);
151+
152+
try {
153+
$this->client($transport)->requestQueue('q1')->batchDeleteRequests($requests);
154+
self::fail('expected InvalidArgumentException');
155+
} catch (InvalidArgumentException $e) {
156+
self::assertStringContainsString('26', $e->getMessage());
157+
}
158+
self::assertSame(0, $transport->callCount());
159+
}
128160
}

0 commit comments

Comments
 (0)