13 — Deadlines, cancellation, and pagination

Goal

Keep requests bounded and make task lists scale beyond a handful of rows.

Step 1: Add pagination messages

In task.proto, add:

message ListTasksRequest {
  string project_id = 1;
  int32 page_size = 2;
  string page_token = 3;
}
message ListTasksResponse {
  repeated Task tasks = 1;
  string next_page_token = 2;
}

Add rpc ListTasks(ListTasksRequest) returns (ListTasksResponse);, then regenerate Go and Dart code using chapter 8's commands.

Step 2: Enforce a page-size limit

At the handler boundary, set a default when page_size <= 0, clamp values above a maximum such as 100, and pass a decoded cursor to the repository. Use a stable ordering such as updated_at DESC, id DESC; the unique ID breaks timestamp ties. Return an opaque continuation token only when another row exists. Do not expose raw SQL fragments as tokens.

For a first implementation, encode the last row's timestamp and ID in a small JSON struct and base64-URL encode it. Validate decoding strictly and return InvalidArgument for a malformed token. For production, sign tokens or store cursor state server-side so clients cannot tamper with it.

Pagination keeps each response and query bounded. A page token is a bookmark that tells the server where to continue; the client treats it as opaque and sends it back unchanged. Keyset pagination uses the last row's sort key instead of counting every earlier row, which usually remains efficient on large tables. A maximum page size prevents a client from asking the server to allocate an unbounded response.

Step 3: Honor request cancellation in Go

Every handler receives ctx context.Context. Pass it to QueryContext/QueryRowContext. For expensive loops, check ctx.Err() between units of work. Do not replace the incoming request context with context.Background() for database calls.

Step 4: Set a client deadline

The Dart gRPC client accepts per-call options:

final response = await client.listTasks(
  request,
  options: CallOptions(timeout: const Duration(seconds: 5)),
);

When search text or filters change, cancel the old request if possible and ignore its result if a newer request has already started. In Flutter, keep a monotonically increasing request number in state; after awaiting, update state only if the number still matches and the widget is mounted.

A deadline is a time budget for the call, not a promise that the server will finish in that time. The client stops waiting after the limit, and the server receives cancellation through the RPC context. Choose a budget based on the operation and network conditions; too short creates avoidable failures, while no limit can leave a screen waiting indefinitely.

Step 5: Load the next page

Store nextPageToken from the response. When the user scrolls near the bottom, request the next page and append tasks. Prevent duplicate in-flight page requests. Reset the token and task list when the project or filter changes.

Checkpoint

Verify that a page never exceeds the configured maximum, the next page has no duplicate IDs, and malformed tokens fail cleanly. Stop a client call early and confirm the database operation receives cancellation.

Practice

Choose between keyset and offset pagination. Explain how changing sort keys can create gaps/duplicates and how your chosen stable cursor behaves under concurrent updates.

results matching ""

    No results matching ""