Add runtime networking APIs

This commit is contained in:
gem
2026-06-09 16:09:19 +08:00
parent 4f36d68b74
commit 7b3c5cb0f5
20 changed files with 936 additions and 6 deletions

View File

@@ -0,0 +1,195 @@
import 'dart:async';
import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
import 'package:flame_lua_runtime/runtime/network/runtime_network_manager.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
void main() {
group('RuntimeNetworkManager', () {
test('supports http and https requests', () async {
final events = <RuntimeEvent>[];
final manager = RuntimeNetworkManager(
eventSink: events.add,
httpClient: MockClient((request) async {
return http.Response(
'ok',
201,
headers: {'content-type': 'text/plain'},
);
}),
);
await manager.httpRequest(
RuntimeHttpRequest(
id: 'http_1',
method: 'GET',
uri: Uri.parse('http://example.com/ping'),
),
);
await manager.httpRequest(
RuntimeHttpRequest(
id: 'https_1',
method: 'POST',
uri: Uri.parse('https://example.com/ping'),
body: 'body',
),
);
expect(events.map((event) => event.type), [
RuntimeNetworkEventType.http,
RuntimeNetworkEventType.http,
]);
expect(events.first.data['ok'], isTrue);
expect(events.first.data['status'], 201);
expect(events.first.data['body'], 'ok');
expect(events.last.data['id'], 'https_1');
});
test('rejects unsupported HTTP schemes with error event', () async {
final diagnostics = RuntimeDiagnostics();
final events = <RuntimeEvent>[];
final manager = RuntimeNetworkManager(
eventSink: events.add,
diagnostics: diagnostics,
httpClient: MockClient((_) async => http.Response('no', 500)),
);
await manager.httpRequest(
RuntimeHttpRequest(
id: 'bad',
method: 'GET',
uri: Uri.parse('ftp://example.com/file'),
),
);
expect(events.single.type, RuntimeNetworkEventType.http);
expect(events.single.data['ok'], isFalse);
expect(events.single.data['error'], contains('http/https'));
expect(
diagnostics.entries.single.type,
RuntimeDiagnosticType.networkError,
);
});
test('supports ws and wss connections plus send and close', () async {
final events = <RuntimeEvent>[];
final controllers = <StreamChannelController<Object?>>[];
addTearDown(() {
for (final controller in controllers) {
controller.local.sink.close();
}
});
final manager = RuntimeNetworkManager(
eventSink: events.add,
webSocketFactory: (_, {protocols}) {
final controller = StreamChannelController<Object?>();
controllers.add(controller);
return _FakeWebSocketChannel(controller.foreign);
},
);
manager.wsConnect(
RuntimeWebSocketConnectRequest(
id: 'ws_1',
uri: Uri.parse('ws://example.com/socket'),
),
);
manager.wsConnect(
RuntimeWebSocketConnectRequest(
id: 'wss_1',
uri: Uri.parse('wss://example.com/socket'),
protocols: const ['game.v1'],
),
);
expect(manager.wsSend('wss_1', 'hello'), isTrue);
controllers.last.local.sink.add('server');
await Future<void>.delayed(Duration.zero);
expect(manager.closeWebSocket('wss_1'), isTrue);
expect(
events.map((event) => event.type),
contains(RuntimeNetworkEventType.wsOpen),
);
expect(
events.where((event) => event.type == RuntimeNetworkEventType.wsOpen),
hasLength(2),
);
expect(
events
.where((event) => event.type == RuntimeNetworkEventType.wsMessage)
.single
.data['message'],
'server',
);
expect(events.last.type, RuntimeNetworkEventType.wsClose);
});
test('rejects unsupported WebSocket schemes with error event', () {
final events = <RuntimeEvent>[];
final manager = RuntimeNetworkManager(eventSink: events.add);
manager.wsConnect(
RuntimeWebSocketConnectRequest(
id: 'bad',
uri: Uri.parse('http://example.com/socket'),
),
);
expect(events.single.type, RuntimeNetworkEventType.wsError);
expect(events.single.data['error'], contains('ws/wss'));
});
});
}
class _FakeWebSocketChannel extends StreamChannelMixin
implements WebSocketChannel {
_FakeWebSocketChannel(StreamChannel<Object?> channel) : _channel = channel;
final StreamChannel<Object?> _channel;
@override
Stream get stream => _channel.stream;
@override
WebSocketSink get sink => _FakeWebSocketSink(_channel.sink);
@override
int? get closeCode => null;
@override
String? get closeReason => null;
@override
String? get protocol => null;
@override
Future<void> get ready => Future<void>.value();
}
class _FakeWebSocketSink implements WebSocketSink {
_FakeWebSocketSink(this._sink);
final StreamSink<Object?> _sink;
@override
void add(Object? event) => _sink.add(event);
@override
void addError(Object error, [StackTrace? stackTrace]) {
_sink.addError(error, stackTrace);
}
@override
Future<void> addStream(Stream stream) => _sink.addStream(stream);
@override
Future<void> close([int? closeCode, String? closeReason]) => _sink.close();
@override
Future<void> get done => _sink.done;
}