feat: move native communication to FFI (#1457)

This commit is contained in:
Levi authored and GitHub committed 2026-09-05 20:18:42 +08:00
1 parent 177c225f16
commit 39cd9034cd
86 files changed
+4558 -886

No files matched your search

@@ -0,0 +1,93 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import '../../../libgopeed_boot.dart';
import '../../../ffi/libgopeed_worker.dart';
import '../../../../util/util.dart';
import '../gopeed_transport.dart';
GopeedTransport createTransport(GopeedTransportConfig config) => NativeGopeedTransport();
class NativeGopeedTransport implements GopeedTransport {
NativeGopeedTransport()
: _externalDio = Dio(
BaseOptions(
contentType: Headers.jsonContentType,
sendTimeout: const Duration(seconds: 5),
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 120),
),
);
final Dio _externalDio;
final LibgopeedWorker? _ffiWorker = Util.isDesktop() ? LibgopeedWorker() : null;
@override
Future<dynamic> request(
String path, {
String method = 'GET',
dynamic data,
Map<String, dynamic>? queryParameters,
}) async {
final request = _RequestParts.from(path, queryParameters);
final requestBody = data == null ? '' : jsonEncode(data);
final payload = _ffiWorker != null
? await _ffiWorker.invoke(method.toUpperCase(), request.path, request.query, requestBody)
: await LibgopeedBoot.instance.invoke(
method.toUpperCase(),
request.path,
query: request.query,
body: requestBody,
);
return jsonDecode(payload);
}
@override
Future<Response<String>> proxyRequest(String uri, {dynamic data, Options? options}) {
return _externalDio.request<String>(
uri,
data: data,
options: (options ?? Options()).copyWith(responseType: ResponseType.plain),
);
}
@override
String join(String path) => path;
}
class _RequestParts {
const _RequestParts(this.path, this.query);
final String path;
final String query;
factory _RequestParts.from(String rawPath, Map<String, dynamic>? queryParameters) {
var path = rawPath;
var query = '';
final queryIndex = rawPath.indexOf('?');
if (queryIndex >= 0) {
path = rawPath.substring(0, queryIndex);
query = rawPath.substring(queryIndex + 1);
}
final extraQuery = _encodeQueryParameters(queryParameters);
if (extraQuery.isNotEmpty) {
query = query.isEmpty ? extraQuery : '$query&$extraQuery';
}
return _RequestParts(path, query);
}
static String _encodeQueryParameters(Map<String, dynamic>? queryParameters) {
if (queryParameters == null || queryParameters.isEmpty) return '';
final segments = <String>[];
queryParameters.forEach((key, value) {
if (value == null) return;
final values = value is Iterable ? value : [value];
for (final item in values) {
if (item == null) continue;
segments.add('${Uri.encodeQueryComponent(key)}=${Uri.encodeQueryComponent(item.toString())}');
}
});
return segments.join('&');
}
}
@@ -0,0 +1,5 @@
import '../gopeed_transport.dart';
GopeedTransport createTransport(GopeedTransportConfig config) {
throw UnsupportedError('Gopeed transport is unavailable on this platform');
}
@@ -0,0 +1,73 @@
import 'package:dio/dio.dart';
import '../gopeed_transport.dart';
GopeedTransport createTransport(GopeedTransportConfig config) => RestGopeedTransport(config);
class RestGopeedTransport implements GopeedTransport {
RestGopeedTransport(GopeedTransportConfig config)
: _dio = Dio(
BaseOptions(
baseUrl: '${Uri.base.origin}/',
contentType: Headers.jsonContentType,
sendTimeout: const Duration(seconds: 5),
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 120),
),
) {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
if (config.apiToken.isNotEmpty) {
options.headers['X-Api-Token'] = config.apiToken;
}
handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
config.onUnauthorized?.call();
}
handler.next(error);
},
),
);
}
final Dio _dio;
@override
Future<dynamic> request(
String path, {
String method = 'GET',
dynamic data,
Map<String, dynamic>? queryParameters,
}) async {
final response = await _dio.request<dynamic>(
path,
data: data,
queryParameters: queryParameters,
options: Options(method: method),
);
return response.data;
}
@override
Future<Response<String>> proxyRequest(String uri, {dynamic data, Options? options}) {
options ??= Options();
options.headers ??= {};
options.headers!['X-Target-Uri'] = uri;
return _dio.request<String>(
'/api/web/proxy?t=${DateTime.now().millisecondsSinceEpoch}',
data: data,
options: options.copyWith(responseType: ResponseType.plain),
);
}
@override
String join(String path) {
final baseUrl = _dio.options.baseUrl;
final cleanBaseUrl = baseUrl.endsWith('/') ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl;
final cleanPath = path.startsWith('/') ? path.substring(1) : path;
return '$cleanBaseUrl/$cleanPath';
}
}
@@ -0,0 +1,29 @@
import 'package:dio/dio.dart';
import 'entry/gopeed_transport_stub.dart'
if (dart.library.html) 'entry/gopeed_transport_web.dart'
if (dart.library.io) 'entry/gopeed_transport_native.dart';
class GopeedTransportConfig {
const GopeedTransportConfig({
required this.network,
required this.address,
required this.apiToken,
this.onUnauthorized,
});
final String network;
final String address;
final String apiToken;
final void Function()? onUnauthorized;
}
abstract interface class GopeedTransport {
Future<dynamic> request(String path, {String method = 'GET', dynamic data, Map<String, dynamic>? queryParameters});
Future<Response<String>> proxyRequest(String uri, {dynamic data, Options? options});
String join(String path);
}
GopeedTransport createGopeedTransport(GopeedTransportConfig config) => createTransport(config);