home / skills / yelmuratoff / agent_sync / networking

networking skill

/.ai/src/skills/networking

This skill helps you implement reliable API calls, robust parsing, and tests by enforcing layered data handling and explicit error mapping.

npx playbooks add skill yelmuratoff/agent_sync --skill networking

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
4.3 KB
---
name: networking
description: When implementing API calls, background parsing, error mapping, and repository/client tests.
---

# Networking (Clients, Parsing, Testing)

## When to use

- Adding a new endpoint for a feature.
- Parsing large JSON responses.
- Introducing retries/timeouts or improving reliability.
- Writing unit tests around repositories and clients.

## Steps

### 1) Keep HTTP in the data layer

Typical flow:

- BLoC calls repository interface (domain)
- repository implementation calls datasource/client (data)
- datasource owns request/response details

Example datasource:

```dart
abstract interface class IOrdersRemoteDataSource {
  Future<List<Map<String, Object?>>> fetchOrders();
}
```

### 2) Map failures into explicit exceptions

Define explicit exception types and convert low-level errors at the boundary:

```dart
final class NetworkException implements Exception {
  const NetworkException(this.message, {this.cause});
  final String message;
  final Object? cause;
}

final class ParseException implements Exception {
  const ParseException(this.message, {this.cause});
  final String message;
  final Object? cause;
}
```

Repository boundary example:

```dart
final class OrdersRepository implements IOrdersRepository {
  OrdersRepository({required this.remote});
  final IOrdersRemoteDataSource remote;

  @override
  Future<List<OrderDto>> getOrders() async {
    try {
      final maps = await remote.fetchOrders();
      return maps.map(OrderDto.fromMap).toList();
    } on FormatException catch (e) {
      throw ParseException('Invalid orders payload', cause: e);
    } catch (e) {
      throw NetworkException('Failed to fetch orders', cause: e);
    }
  }
}
```

### 3) Parse large payloads off the UI thread

Use background parsing for large JSON:

```dart
import 'package:flutter/foundation.dart';

List<OrderDto> parseOrders(List<Map<String, Object?>> maps) =>
    maps.map(OrderDto.fromMap).toList();

Future<List<OrderDto>> parseOrdersInBackground(List<Map<String, Object?>> maps) =>
    compute(parseOrders, maps);
```

For heavier work, prefer `Isolate.run` (Dart 3) where available.

### 4) Unit test repositories with mocked datasources

Prefer mocking the datasource/client (not making real HTTP calls):

```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class _RemoteMock extends Mock implements IOrdersRemoteDataSource {}

void main() {
  test('getOrders returns parsed DTOs', () async {
    final remote = _RemoteMock();
    when(() => remote.fetchOrders()).thenAnswer(
      (_) async => [
        {'id': '1', 'createdAt': '2026-01-01T00:00:00.000Z', 'timeout': 1000},
      ],
    );

    final repo = OrdersRepository(remote: remote);
    final result = await repo.getOrders();

    expect(result.length, 1);
    expect(result.first.id, '1');
  });
}
```

### 5) Implement auth token refresh (three-component pattern)

When the API requires authentication with token refresh, use three collaborating components:

**TokenStorage** (backed by `flutter_secure_storage`, never `SharedPreferences`):

```dart
abstract interface class TokenStorage<T> {
  Future<T?> load();
  Future<void> save(T token);
  Future<void> clear();
  Stream<T?> getStream(); // broadcasts changes to all listeners
}
```

**AuthorizationClient** (validates expiry locally, calls refresh endpoint):

```dart
abstract interface class AuthorizationClient {
  /// Returns valid access token, refreshing if expired.
  /// Throws [RevokeTokenException] if refresh fails.
  Future<String> getValidAccessToken();
}
```

**OAuthInterceptor** key constraints:
- Attaches `Authorization: Bearer <token>` before each request.
- On 401: acquires a sequential lock so only one refresh fires even when multiple requests receive 401 simultaneously. All concurrent requests queue and retry with the new token.
- On `RevokeTokenException`: calls `TokenStorage.clear()` and triggers logout.

Key constraint: the sequential lock prevents N concurrent 401 responses from triggering N separate refresh calls. Only the first refresh executes; others await its result.

Test cases required:
- Happy path: valid token attached, 200 returned.
- Expired token: refresh called once, original request retried with new token.
- Concurrent 401s: refresh called exactly once despite multiple 401s.
- Revoked token: `TokenStorage.clear()` called, logout triggered.

Overview

This skill packages guidance and utilities for implementing networked data layers: clients, parsing, error mapping, and repository/client tests. It focuses on keeping HTTP concerns in the data layer, converting low-level failures into explicit exceptions, offloading heavy parsing, and making auth token refresh robust and testable. The goal is predictable, testable networking code that scales in Flutter/Dart apps.

How this skill works

The pattern places HTTP calls inside datasource/client implementations that the repository calls, keeping domain code agnostic of transport details. Low-level errors (format issues, network failures) are mapped to explicit exception types at the repository boundary. Large JSON parsing runs off the UI thread using compute or Isolate.run. For authenticated APIs, a three-component pattern (TokenStorage, AuthorizationClient, OAuthInterceptor) centralizes token lifecycle and serializes refresh behavior.

When to use it

  • Adding a new API endpoint or client integration
  • Parsing large JSON payloads that can block the UI
  • Introducing retries, timeouts, or improving network reliability
  • Implementing authenticated requests with token refresh
  • Writing unit tests for repositories and HTTP clients

Best practices

  • Keep HTTP logic confined to the data layer (datasource/client) and expose simple repository interfaces
  • Convert FormatException/network errors to explicit ParseException/NetworkException at the repository boundary
  • Offload heavy JSON parsing to compute or Isolate.run to avoid jank
  • Mock the datasource/client in unit tests—do not perform real HTTP calls
  • Store sensitive tokens in secure storage and never in SharedPreferences
  • Use a sequential lock in the interceptor so only one token refresh runs on concurrent 401s

Example use cases

  • Repository fetchOrders calls IOrdersRemoteDataSource.fetchOrders and maps results to DTOs with error translation
  • Parsing thousands of items in background via compute(parseOrders, maps) or Isolate.run for heavier workloads
  • Unit tests that mock the remote datasource to assert repository parsing and error mapping
  • OAuth flow where AuthorizationClient returns valid tokens and OAuthInterceptor serializes refreshes
  • Tests asserting single refresh call when multiple requests receive 401 concurrently

FAQ

Why map low-level errors into explicit exceptions?

Explicit exceptions (e.g., NetworkException, ParseException) provide a clear, testable contract at the repository boundary and avoid leaking transport-specific errors to higher layers.

When should I use compute vs Isolate.run?

Use compute for lightweight JSON mapping on older Dart versions; prefer Isolate.run for heavier CPU-bound parsing and when running on Dart 3+ for better control.