synor/sdk/flutter/lib/src/job.dart
Gulshan Yadav 3e68f72743 fix: resolve 35 clippy warnings across Rust and Dart codebases
## Rust Fixes (35 warnings resolved)
- Remove unused imports (synor-vm, synor-bridge, tests)
- Remove unused variables and prefix intentional ones with underscore
- Use derive for Default implementations (6 structs)
- Replace manual is_multiple_of with standard method (3 occurrences)
- Fix needless borrows by using direct expressions (12 occurrences)
- Suppress false-positive variant assignment warnings with allow attributes
- Fix Default field initialization pattern in synor-crypto
- Rename MerklePath::to_string() to path() to avoid conflict with Display trait

## Flutter/Dart Fixes
- Add const constructors for immutable objects (8 instances)
- Remove unused imports (dart:convert, collection package, tensor.dart)

## Impact
- Reduced clippy warnings from 49 to 10 (79% reduction)
- Remaining 10 warnings are "too many arguments" requiring architectural refactoring
- All library code compiles successfully
- Code quality and maintainability improved
2026-01-26 17:08:57 +05:30

393 lines
9.8 KiB
Dart

/// Job tracking for Synor Compute SDK
library synor_compute.job;
import 'dart:async';
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'types.dart';
/// Result of a compute job
class JobResult<T> {
/// Unique job identifier
final String jobId;
/// Current job status
final JobStatus status;
/// Result data (if completed)
final T? result;
/// Error message (if failed)
final String? error;
/// Execution time in milliseconds
final int? executionTimeMs;
/// Cost in credits
final double? cost;
/// Processor that executed the job
final ProcessorType? processor;
/// Metadata from execution
final Map<String, dynamic>? metadata;
const JobResult({
required this.jobId,
required this.status,
this.result,
this.error,
this.executionTimeMs,
this.cost,
this.processor,
this.metadata,
});
/// Whether the job completed successfully
bool get isSuccess => status == JobStatus.completed && result != null;
/// Whether the job failed
bool get isFailed => status == JobStatus.failed;
/// Whether the job is still running
bool get isRunning => !status.isTerminal;
factory JobResult.fromJson(
Map<String, dynamic> json,
T Function(dynamic)? resultParser,
) {
final status = JobStatus.fromString(json['status'] as String);
T? result;
if (json['result'] != null && resultParser != null) {
result = resultParser(json['result']);
}
return JobResult<T>(
jobId: json['job_id'] as String,
status: status,
result: result,
error: json['error'] as String?,
executionTimeMs: json['execution_time_ms'] as int?,
cost: (json['cost'] as num?)?.toDouble(),
processor: json['processor'] != null
? ProcessorType.fromString(json['processor'] as String)
: null,
metadata: json['metadata'] as Map<String, dynamic>?,
);
}
/// Transform the result to a different type
JobResult<R> map<R>(R Function(T) transform) {
return JobResult<R>(
jobId: jobId,
status: status,
result: result != null ? transform(result as T) : null,
error: error,
executionTimeMs: executionTimeMs,
cost: cost,
processor: processor,
metadata: metadata,
);
}
@override
String toString() {
if (isSuccess) {
return 'JobResult(id: $jobId, status: ${status.value}, '
'time: ${executionTimeMs}ms, cost: \$${cost?.toStringAsFixed(6)})';
} else if (isFailed) {
return 'JobResult(id: $jobId, status: ${status.value}, error: $error)';
}
return 'JobResult(id: $jobId, status: ${status.value})';
}
}
/// Job status update event
class JobStatusUpdate {
final String jobId;
final JobStatus status;
final double? progress;
final String? message;
final DateTime timestamp;
const JobStatusUpdate({
required this.jobId,
required this.status,
this.progress,
this.message,
required this.timestamp,
});
factory JobStatusUpdate.fromJson(Map<String, dynamic> json) {
return JobStatusUpdate(
jobId: json['job_id'] as String,
status: JobStatus.fromString(json['status'] as String),
progress: (json['progress'] as num?)?.toDouble(),
message: json['message'] as String?,
timestamp: json['timestamp'] != null
? DateTime.parse(json['timestamp'] as String)
: DateTime.now(),
);
}
}
/// Job handle for tracking and managing a submitted job
class Job<T> {
final String jobId;
final String _baseUrl;
final Map<String, String> _headers;
final T Function(dynamic)? _resultParser;
WebSocketChannel? _wsChannel;
StreamController<JobStatusUpdate>? _statusController;
JobResult<T>? _cachedResult;
bool _isDisposed = false;
Job({
required this.jobId,
required String baseUrl,
required Map<String, String> headers,
T Function(dynamic)? resultParser,
}) : _baseUrl = baseUrl,
_headers = headers,
_resultParser = resultParser;
/// Stream of status updates for this job
Stream<JobStatusUpdate> get statusUpdates {
_statusController ??= StreamController<JobStatusUpdate>.broadcast(
onListen: _startWebSocket,
onCancel: _stopWebSocket,
);
return _statusController!.stream;
}
void _startWebSocket() {
if (_isDisposed) return;
final wsUrl = _baseUrl
.replaceFirst('http://', 'ws://')
.replaceFirst('https://', 'wss://');
_wsChannel = WebSocketChannel.connect(
Uri.parse('$wsUrl/jobs/$jobId/stream'),
);
_wsChannel!.stream.listen(
(data) {
if (_isDisposed) return;
try {
final json = jsonDecode(data as String) as Map<String, dynamic>;
final update = JobStatusUpdate.fromJson(json);
_statusController?.add(update);
if (update.status.isTerminal) {
_stopWebSocket();
}
} catch (e) {
// Ignore parse errors
}
},
onError: (error) {
if (!_isDisposed) {
_statusController?.addError(error);
}
},
onDone: _stopWebSocket,
);
}
void _stopWebSocket() {
_wsChannel?.sink.close();
_wsChannel = null;
}
/// Poll for job result (for environments without WebSocket support)
Future<JobResult<T>> poll({
Duration interval = const Duration(milliseconds: 500),
Duration timeout = const Duration(minutes: 5),
}) async {
if (_cachedResult?.status.isTerminal == true) {
return _cachedResult!;
}
final endTime = DateTime.now().add(timeout);
final client = _createHttpClient();
try {
while (DateTime.now().isBefore(endTime)) {
final response = await client.get(
Uri.parse('$_baseUrl/jobs/$jobId'),
headers: _headers,
);
if (response.statusCode != 200) {
throw SynorException(
'Failed to poll job status',
statusCode: response.statusCode,
);
}
final json = jsonDecode(response.body) as Map<String, dynamic>;
final result = JobResult<T>.fromJson(json, _resultParser);
if (result.status.isTerminal) {
_cachedResult = result;
return result;
}
await Future.delayed(interval);
}
throw SynorException('Job polling timed out after $timeout');
} finally {
client.close();
}
}
/// Wait for job completion with automatic strategy selection
Future<JobResult<T>> wait({
Duration timeout = const Duration(minutes: 5),
bool useWebSocket = true,
}) async {
if (_cachedResult?.status.isTerminal == true) {
return _cachedResult!;
}
if (useWebSocket) {
try {
final completer = Completer<JobResult<T>>();
late StreamSubscription<JobStatusUpdate> subscription;
subscription = statusUpdates.listen(
(update) async {
if (update.status.isTerminal && !completer.isCompleted) {
final result = await poll(
interval: Duration.zero,
timeout: const Duration(seconds: 10),
);
completer.complete(result);
await subscription.cancel();
}
},
onError: (error) {
if (!completer.isCompleted) {
completer.completeError(error);
}
},
);
return await completer.future.timeout(
timeout,
onTimeout: () {
subscription.cancel();
throw SynorException('Job wait timed out after $timeout');
},
);
} catch (e) {
// Fall back to polling if WebSocket fails
return poll(timeout: timeout);
}
}
return poll(timeout: timeout);
}
/// Cancel the job
Future<bool> cancel() async {
final client = _createHttpClient();
try {
final response = await client.post(
Uri.parse('$_baseUrl/jobs/$jobId/cancel'),
headers: _headers,
);
if (response.statusCode == 200) {
_cachedResult = JobResult<T>(
jobId: jobId,
status: JobStatus.cancelled,
);
return true;
}
return false;
} finally {
client.close();
}
}
/// Get current job status
Future<JobStatus> getStatus() async {
final client = _createHttpClient();
try {
final response = await client.get(
Uri.parse('$_baseUrl/jobs/$jobId/status'),
headers: _headers,
);
if (response.statusCode != 200) {
throw SynorException(
'Failed to get job status',
statusCode: response.statusCode,
);
}
final json = jsonDecode(response.body) as Map<String, dynamic>;
return JobStatus.fromString(json['status'] as String);
} finally {
client.close();
}
}
/// Dispose resources
void dispose() {
_isDisposed = true;
_stopWebSocket();
_statusController?.close();
_statusController = null;
}
// Creates an HTTP client - in a real app, use http package
dynamic _createHttpClient() {
// This is a placeholder - actual implementation uses http package
throw UnimplementedError('HTTP client should be injected');
}
}
/// Batch job operations
class JobBatch<T> {
final List<Job<T>> jobs;
JobBatch(this.jobs);
/// Wait for all jobs to complete
Future<List<JobResult<T>>> waitAll({
Duration timeout = const Duration(minutes: 10),
}) async {
return Future.wait(
jobs.map((job) => job.wait(timeout: timeout)),
);
}
/// Wait for first job to complete
Future<JobResult<T>> waitAny({
Duration timeout = const Duration(minutes: 5),
}) async {
return Future.any(
jobs.map((job) => job.wait(timeout: timeout)),
);
}
/// Cancel all jobs
Future<void> cancelAll() async {
await Future.wait(jobs.map((job) => job.cancel()));
}
/// Dispose all job resources
void dispose() {
for (final job in jobs) {
job.dispose();
}
}
}