Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## 0.3.0

Breaking: `RequestQueueClient` methods that previously returned a raw `JsonObject`/took an untyped
`object` now use typed models, matching the OpenAPI-documented response schemas and the typing the
sibling clients already apply to this same resource:

- `ListAndLockHeadAsync` now returns `LockedRequestQueueHead` (was `JsonObject`).
- `ProlongRequestLockAsync` now returns `RequestLockInfo` (was `JsonObject`).
- `UnlockRequestsAsync` now returns `UnlockRequestsResult` (was `JsonObject`).
- `ListRequestsAsync` now returns `RequestQueueRequestsPage` (was `JsonObject`).
- `BatchDeleteRequestsAsync` now takes `IReadOnlyList<RequestQueueRequest>` and returns
`BatchDeleteResult` (was `object requests` / `JsonObject`).
- `RequestQueueRequest` gained `RetryCount`/`LockExpiresAt` properties, populated on requests returned
by `ListAndLockHeadAsync`/`ListRequestsAsync`.
- `RequestQueueHead` and `LockedRequestQueueHead` gained the previously-missing `QueueModifiedAt`
field (present in the OpenAPI spec and the reference client, but not yet exposed by this client).

## 0.2.0

- Bumped `ApifyClientVersion.ApiSpecVersion` to the Apify OpenAPI spec `v2-2026-08-05T133145Z` and the
Expand Down
55 changes: 55 additions & 0 deletions docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ A read/write model (request input and response). Fields set to `null` are omitte
| `UniqueKey` | `string?` | The key used to deduplicate the request within the queue. |
| `Method` | `string?` | HTTP method (defaults to `GET` on the server). |
| `UserData` | `JsonNode?` | Arbitrary user-defined JSON payload attached to the request. |
| `RetryCount` | `long?` | Number of times the request has been retried (assigned by the queue). |
| `LockExpiresAt` | `string?` | ISO 8601 lock expiry; only set on requests returned by `ListAndLockHeadAsync`. |

## `RequestQueueHead`

Expand All @@ -212,8 +214,61 @@ The head (front) of a request queue.
|---|---|---|
| `Items` | `IReadOnlyList<RequestQueueRequest>` | The requests at the head of the queue. |
| `Limit` | `long` | The page-size limit that was applied. |
| `QueueModifiedAt` | `string?` | ISO 8601 timestamp of the last modification to the queue. |
| `HadMultipleClients` | `bool` | `true` if more than one client has accessed the queue (concurrency hint). |

## `LockedRequestQueueHead`

The result of `RequestQueueClient.ListAndLockHeadAsync()`: a batch of requests locked for exclusive
processing. Each item's `RequestQueueRequest.LockExpiresAt` holds its individual lock expiry.

| Property | Type | Description |
|---|---|---|
| `Items` | `IReadOnlyList<RequestQueueRequest>` | The locked requests. |
| `Limit` | `long` | The maximum number of requests requested. |
| `QueueModifiedAt` | `string?` | ISO 8601 timestamp of the last modification to the queue. |
| `HadMultipleClients` | `bool` | `true` if more than one client has accessed the queue. |
| `LockSecs` | `long` | The lock duration applied to every returned request, in seconds. |
| `QueueHasLockedRequests` | `bool?` | Whether the queue has any requests locked by any client. |
| `ClientKey` | `string?` | The client key used to acquire the locks. |

## `RequestLockInfo`

The result of `RequestQueueClient.ProlongRequestLockAsync()`.

| Property | Type | Description |
|---|---|---|
| `LockExpiresAt` | `string?` | ISO 8601 timestamp the (possibly just-extended) lock expires at. |

## `UnlockRequestsResult`

The result of `RequestQueueClient.UnlockRequestsAsync()`.

| Property | Type | Description |
|---|---|---|
| `UnlockedCount` | `long` | Number of requests that were unlocked. |

## `RequestQueueRequestsPage`

One cursor-paginated page of `RequestQueueClient.ListRequestsAsync()`.

| Property | Type | Description |
|---|---|---|
| `Items` | `IReadOnlyList<RequestQueueRequest>` | The requests in this page. |
| `Limit` | `long` | The page-size limit that was applied. |
| `ExclusiveStartId` | `string?` | Deprecated by the API in favor of `Cursor`/`NextCursor`. |
| `Cursor` | `string?` | The cursor that produced this page. |
| `NextCursor` | `string?` | Cursor to pass to fetch the next page, or `null` if this is the last page. |

## `BatchDeleteResult`

The aggregate result of `RequestQueueClient.BatchDeleteRequestsAsync()`.

| Property | Type | Description |
|---|---|---|
| `ProcessedRequests` | `IReadOnlyList<RequestQueueRequest>` | Requests successfully deleted. |
| `UnprocessedRequests` | `IReadOnlyList<RequestQueueRequest>` | Requests that failed to delete and can be retried. |

## `RequestQueueOperationInfo`

The result of adding/updating a single request.
Expand Down
14 changes: 8 additions & 6 deletions docs/storages.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,18 @@ options set a stable `ClientKey` (required to manage locks the client created) a
`UpdateRequestAsync(RequestQueueRequest request, bool forefront = false)` → `RequestQueueOperationInfo`;
`DeleteRequestAsync(string id)` (no return value).
- `ListHeadAsync(int? limit = null)` → `RequestQueueHead`;
`ListAndLockHeadAsync(int lockSecs, int? limit = null)`.
`ListAndLockHeadAsync(int lockSecs, int? limit = null)` → `LockedRequestQueueHead` (each item's
`RequestQueueRequest.LockExpiresAt`/`RetryCount` is populated).
- `BatchAddRequestsAsync(IReadOnlyList<RequestQueueRequest> requests, bool forefront = false, BatchAddRequestsOptions? options = null)`
→ `BatchAddResult` — auto-chunks by count (25) and payload size (~9 MiB) and retries unprocessed
requests. Every request needs a non-empty `UniqueKey`.
- `BatchDeleteRequestsAsync(object requests)` → `JsonObject` — delete a batch of requests in one call
(`requests` is any JSON-serializable list of requests/keys to remove).
- `ListRequestsAsync(ListRequestsOptions? options = null)` → `JsonObject` and
- `BatchDeleteRequestsAsync(IReadOnlyList<RequestQueueRequest> requests)` → `BatchDeleteResult` — delete a
batch of requests in one call; each entry identifies the request to delete via `Id` and/or `UniqueKey`.
- `ListRequestsAsync(ListRequestsOptions? options = null)` → `RequestQueueRequestsPage` and
`PaginateRequestsAsync(PaginateRequestsOptions? options = null)` → `IAsyncEnumerable<RequestQueueRequest>`.
- Lock management: `ProlongRequestLockAsync(string id, int lockSecs, bool forefront = false)` → `JsonObject`,
`DeleteRequestLockAsync(string id, bool forefront = false)`, `UnlockRequestsAsync()` → `JsonObject`.
- Lock management: `ProlongRequestLockAsync(string id, int lockSecs, bool forefront = false)` →
`RequestLockInfo`, `DeleteRequestLockAsync(string id, bool forefront = false)`,
`UnlockRequestsAsync()` → `UnlockRequestsResult`.
- `WithClientKey(string clientKey)` → `RequestQueueClient` — returns a copy of this client bound to the
given client key (a fluent alternative to passing `RequestQueueClientOptions.ClientKey` on
`client.RequestQueue(id, options)`); the client key ties lock ownership to this client.
Expand Down
2 changes: 1 addition & 1 deletion src/Apify.Client/Apify.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<!-- NuGet package metadata (see the publish workflow). -->
<PackageId>Apify.Client</PackageId>
<Version>0.2.0</Version>
<Version>0.3.0</Version>
<Authors>Apify</Authors>
<Company>Apify</Company>
<Product>Apify API client for .NET</Product>
Expand Down
2 changes: 1 addition & 1 deletion src/Apify.Client/ApifyClientVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static class ApifyClientVersion
/// The semantic version of this client library (see https://semver.org/). Changes to the public
/// interface other than additive ones are considered breaking changes.
/// </summary>
public const string ClientVersion = "0.2.0";
public const string ClientVersion = "0.3.0";

/// <summary>
/// The version of the Apify OpenAPI specification this client was generated and verified against.
Expand Down
46 changes: 46 additions & 0 deletions src/Apify.Client/Models/BatchDeleteResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Text.Json.Nodes;
using Apify.Client.Internal;

namespace Apify.Client.Models;

/// <summary>
/// The result of a batch request-delete: the requests that were successfully removed and the ones that
/// could not be (and can be retried).
/// </summary>
public sealed class BatchDeleteResult
{
private BatchDeleteResult(IReadOnlyList<RequestQueueRequest> processed, IReadOnlyList<RequestQueueRequest> unprocessed)
{
ProcessedRequests = processed;
UnprocessedRequests = unprocessed;
}

/// <summary>Builds a result from the decoded response object.</summary>
/// <param name="data">The decoded response object.</param>
public static BatchDeleteResult FromData(JsonNode? data)
{
var obj = JsonValues.AsObject(data);
return new BatchDeleteResult(Hydrate(obj, "processedRequests"), Hydrate(obj, "unprocessedRequests"));
}

private static List<RequestQueueRequest> Hydrate(JsonObject obj, string key)
{
var items = new List<RequestQueueRequest>();
if (obj.TryGetPropertyValue(key, out var node) && node is JsonArray array)
{
foreach (var item in array)
{
items.Add(RequestQueueRequest.FromJsonObject(item as JsonObject ?? new JsonObject()));
}
}

return items;
}

/// <summary>The requests that were successfully deleted from the queue.</summary>
public IReadOnlyList<RequestQueueRequest> ProcessedRequests { get; }

/// <summary>The requests that failed to be deleted and can be retried.</summary>
public IReadOnlyList<RequestQueueRequest> UnprocessedRequests { get; }
}
69 changes: 69 additions & 0 deletions src/Apify.Client/Models/LockedRequestQueueHead.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Text.Json.Nodes;
using Apify.Client.Internal;

namespace Apify.Client.Models;

/// <summary>A batch of requests from the head of a request queue, locked for exclusive processing.</summary>
public sealed class LockedRequestQueueHead
{
private LockedRequestQueueHead(
IReadOnlyList<RequestQueueRequest> items,
long limit,
string? queueModifiedAt,
bool hadMultipleClients,
long lockSecs,
bool? queueHasLockedRequests,
string? clientKey)
{
Items = items;
Limit = limit;
QueueModifiedAt = queueModifiedAt;
HadMultipleClients = hadMultipleClients;
LockSecs = lockSecs;
QueueHasLockedRequests = queueHasLockedRequests;
ClientKey = clientKey;
}

/// <summary>Builds a locked head from the decoded response object.</summary>
/// <param name="data">The decoded response object.</param>
public static LockedRequestQueueHead FromData(JsonNode? data)
{
var obj = JsonValues.AsObject(data);
var items = new List<RequestQueueRequest>();
foreach (var item in JsonValues.ObjectItems(obj))
{
items.Add(RequestQueueRequest.FromJsonObject(item));
}

return new LockedRequestQueueHead(
items,
JsonValues.IntOr(obj, "limit", items.Count),
JsonValues.String(obj, "queueModifiedAt"),
JsonValues.BoolOr(obj, "hadMultipleClients", false),
JsonValues.IntOr(obj, "lockSecs", 0),
obj.ContainsKey("queueHasLockedRequests") ? JsonValues.BoolOr(obj, "queueHasLockedRequests", false) : null,
JsonValues.String(obj, "clientKey"));
}

/// <summary>The locked requests from the head of the queue. Each carries its own <c>LockExpiresAt</c>.</summary>
public IReadOnlyList<RequestQueueRequest> Items { get; }

/// <summary>The maximum number of requests requested.</summary>
public long Limit { get; }

/// <summary>ISO 8601 timestamp of the last modification to the queue.</summary>
public string? QueueModifiedAt { get; }

/// <summary>Whether multiple clients have accessed the queue.</summary>
public bool HadMultipleClients { get; }

/// <summary>The lock duration applied to every returned request, in seconds.</summary>
public long LockSecs { get; }

/// <summary>Whether the queue has any requests locked by any client (this one or another).</summary>
public bool? QueueHasLockedRequests { get; }

/// <summary>The client key used to acquire the locks.</summary>
public string? ClientKey { get; }
}
24 changes: 24 additions & 0 deletions src/Apify.Client/Models/RequestLockInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Text.Json.Nodes;
using Apify.Client.Internal;

namespace Apify.Client.Models;

/// <summary>The result of prolonging a request lock: the new lock expiry.</summary>
public sealed class RequestLockInfo
{
private RequestLockInfo(string? lockExpiresAt)
{
LockExpiresAt = lockExpiresAt;
}

/// <summary>Builds a lock info from the decoded response object.</summary>
/// <param name="data">The decoded response object.</param>
public static RequestLockInfo FromData(JsonNode? data)
{
var obj = JsonValues.AsObject(data);
return new RequestLockInfo(JsonValues.String(obj, "lockExpiresAt"));
}

/// <summary>When the (possibly just-extended) lock expires (ISO-8601 string).</summary>
public string? LockExpiresAt { get; }
}
7 changes: 6 additions & 1 deletion src/Apify.Client/Models/RequestQueueHead.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ namespace Apify.Client.Models;
/// <summary>The head (front) of a request queue.</summary>
public sealed class RequestQueueHead
{
private RequestQueueHead(IReadOnlyList<RequestQueueRequest> items, long limit, bool hadMultipleClients)
private RequestQueueHead(IReadOnlyList<RequestQueueRequest> items, long limit, string? queueModifiedAt, bool hadMultipleClients)
{
Items = items;
Limit = limit;
QueueModifiedAt = queueModifiedAt;
HadMultipleClients = hadMultipleClients;
}

Expand All @@ -28,6 +29,7 @@ public static RequestQueueHead FromData(JsonNode? data)
return new RequestQueueHead(
items,
JsonValues.IntOr(obj, "limit", items.Count),
JsonValues.String(obj, "queueModifiedAt"),
JsonValues.BoolOr(obj, "hadMultipleClients", false));
}

Expand All @@ -37,6 +39,9 @@ public static RequestQueueHead FromData(JsonNode? data)
/// <summary>The maximum number of requests requested.</summary>
public long Limit { get; }

/// <summary>ISO 8601 timestamp of the last modification to the queue.</summary>
public string? QueueModifiedAt { get; }

/// <summary>Whether multiple clients have accessed the queue.</summary>
public bool HadMultipleClients { get; }
}
30 changes: 30 additions & 0 deletions src/Apify.Client/Models/RequestQueueRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,36 @@ public string? Method
set => SetString("method", value);
}

/// <summary>
/// The number of times this request has been retried after a failed processing attempt (assigned by
/// the API; absent on create).
/// </summary>
public long? RetryCount
{
get => GetInt("retryCount");
set
{
if (value is null)
{
ToJsonObject().Remove("retryCount");
}
else
{
ToJsonObject()["retryCount"] = value.Value;
}
}
}

/// <summary>
/// When this request's processing lock expires (ISO-8601 string). Only present on requests returned
/// by <see cref="Resources.RequestQueueClient.ListAndLockHeadAsync"/>.
/// </summary>
public string? LockExpiresAt
{
get => GetString("lockExpiresAt");
set => SetString("lockExpiresAt", value);
}

/// <summary>Arbitrary user-attached metadata.</summary>
public JsonNode? UserData
{
Expand Down
Loading
Loading