api_command_queue 0.2.0 copy "api_command_queue: ^0.2.0" to clipboard
api_command_queue: ^0.2.0 copied to clipboard

Pure Dart command queue primitives for retryable API work, terminal failures, and offline-first orchestration.

example/main.dart

import 'dart:async';

import 'package:api_command_queue/api_command_queue.dart';

final _validationFailureRule = ApiCommandTerminalFailureRule<ExamplePayload>(
  statusCodes: const {422},
  dataMatches: (data) => data?.title == 'validation_rejected',
);

Future<void> main() async {
  await runExample();
}

Future<void> runExample() async {
  final queue = ExampleQueue();
  final orchestrator = ApiCommandOrchestrator(
    commandQueues: {ExampleCommand: queue},
    processingEnabled: false,
  );

  final optimistic =
      orchestrator.enqueue(ExampleCommand.create('cmd-1', 'Publish package'));
  print('Optimistic title: ${optimistic?.title}');

  orchestrator.setProcessingEnabled(true);

  final result = await queue.results.first.timeout(const Duration(seconds: 1));
  print(
    'Completed ${result.command.uuid} with status '
    '${result.response.status} success=${result.success}',
  );

  await orchestrator.close();
}

final class ExamplePayload {
  const ExamplePayload(this.title);

  final String title;

  Map<String, dynamic> toJson() => {'title': title};

  static ExamplePayload fromJson(Object? json) {
    final map = (json as Map).cast<String, dynamic>();
    return ExamplePayload(map['title'] as String);
  }
}

final class ExampleCommand extends ApiCommand<ExamplePayload,
    ApiCommandRequest<ExamplePayload>, ExamplePayload, ExampleCommand> {
  const ExampleCommand._({
    required super.uuid,
    required super.request,
    required super.strategy,
    required super.status,
    required super.attemptCount,
    required super.firstFailureAt,
    required super.lastUpdated,
    super.apiResponse,
  });

  factory ExampleCommand.create(String id, String title) {
    return ExampleCommand._(
      uuid: id,
      request: ApiCommandRequest(
        ApiCommandRequestMethod.post,
        ExamplePayload(title),
      ),
      strategy: CommandReplaceStrategy.multiple,
      status: ApiCommandStatus.idle,
      attemptCount: 0,
      firstFailureAt: null,
      lastUpdated: DateTime.now(),
    );
  }

  @override
  Future<ApiCommandResponse<ExamplePayload>?> execute() async {
    await Future<void>.delayed(const Duration(milliseconds: 10));
    if (request.data.title == 'Already exists') {
      return ApiCommandResponse<ExamplePayload>(
        null,
        false,
        status: 409,
        error: 'duplicate title',
      );
    }
    if (request.data.title.trim().isEmpty) {
      return ApiCommandResponse<ExamplePayload>(
        const ExamplePayload('validation_rejected'),
        false,
        status: 422,
      );
    }
    return ApiCommandResponse<ExamplePayload>(request.data, false, status: 201);
  }

  @override
  bool isTerminalFailure(ApiCommandResponse<ExamplePayload?> response) {
    return _validationFailureRule.matches(response);
  }

  @override
  ExamplePayload? offlineResult() => request.data;

  @override
  ExamplePayload mergePayload(ExamplePayload update) => update;

  @override
  ExampleCommand copyWith({
    ApiCommandRequest<ExamplePayload>? request,
    CommandReplaceStrategy? strategy,
    ApiCommandStatus? status,
    DateTime? lastUpdated,
    int? attemptCount,
    DateTime? firstFailureAt,
    ApiCommandResponse<ExamplePayload?>? apiResponse,
  }) {
    return ExampleCommand._(
      uuid: uuid,
      request: request ?? this.request,
      strategy: strategy ?? this.strategy,
      status: status ?? this.status,
      attemptCount: attemptCount ?? this.attemptCount,
      firstFailureAt: firstFailureAt ?? this.firstFailureAt,
      lastUpdated: lastUpdated ?? this.lastUpdated,
      apiResponse: apiResponse ?? this.apiResponse,
    );
  }

  @override
  Object? requestDataToJson(ExamplePayload requestData) => requestData.toJson();

  @override
  Object? responseDataToJson(ExamplePayload? responseData) =>
      responseData?.toJson();

  static ExampleCommand fromJson(Map<String, dynamic> json) {
    return ExampleCommand._(
      uuid: json['id'] as String,
      request: ApiCommandRequest.fromJson(
        (json['request'] as Map).cast<String, dynamic>(),
        ExamplePayload.fromJson,
      ),
      strategy: CommandReplaceStrategy.values.firstWhere(
        (value) => value.name == json['strategy'] as String,
      ),
      status: ApiCommandStatus.values.firstWhere(
        (value) => value.name == json['status'] as String,
      ),
      attemptCount: json['attemptCount'] as int,
      firstFailureAt: json['firstFailureAt'] == null
          ? null
          : DateTime.parse(json['firstFailureAt'] as String),
      lastUpdated: DateTime.parse(json['lastUpdated'] as String),
      apiResponse: json['apiResponse'] == null
          ? null
          : ApiCommandResponse.fromJson(
              (json['apiResponse'] as Map).cast<String, dynamic>(),
              ExamplePayload.fromJson,
            ),
    );
  }
}

final class ExampleQueue extends ApiCommandQueue<ExamplePayload,
    ApiCommandRequest<ExamplePayload>, ExamplePayload, ExampleCommand> {
  ExampleQueue()
      : super(
          commandFromJson: ExampleCommand.fromJson,
          terminalFailurePredicate:
              const ApiCommandTerminalFailureRule<ExamplePayload>(
            statusCodes: {401, 403, 409},
          ).matches,
          retryPolicy: const ExponentialBackoffRetryPolicy(
            maxAttempts: 1,
            initialDelay: Duration.zero,
            backoffFactor: 1,
            maxDelay: Duration.zero,
          ),
        );
}
1
likes
160
points
101
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Pure Dart command queue primitives for retryable API work, terminal failures, and offline-first orchestration.

Repository (GitHub)
View/report issues

Topics

#command-queue #offline-first #retry #state-management

License

MIT (license)

Dependencies

meta, uuid

More

Packages that depend on api_command_queue