Add bidirectional host bridge

This commit is contained in:
gem
2026-06-09 16:26:37 +08:00
parent 7b3c5cb0f5
commit 0d4fbd030c
17 changed files with 632 additions and 3 deletions

View File

@@ -0,0 +1,228 @@
import 'dart:async' as async;
import '../diagnostics/runtime_diagnostics.dart';
import '../models/runtime_event.dart';
typedef RuntimeHostCallHandler =
async.FutureOr<Object?> Function(RuntimeHostCall call);
typedef RuntimeHostNotifyHandler =
void Function(RuntimeHostNotification notification);
class RuntimeHostBridge {
const RuntimeHostBridge({this.handlers = const {}, this.onNotify});
final Map<String, RuntimeHostCallHandler> handlers;
final RuntimeHostNotifyHandler? onNotify;
}
class RuntimeHostCall {
const RuntimeHostCall({required this.id, required this.method, this.data});
final String id;
final String method;
final Object? data;
}
class RuntimeHostNotification {
const RuntimeHostNotification({required this.method, this.data});
final String method;
final Object? data;
}
class RuntimeHostBridgeManager {
RuntimeHostBridgeManager({
required RuntimeHostBridge bridge,
required void Function(RuntimeEvent event) eventSink,
RuntimeDiagnostics? diagnostics,
}) : _bridge = bridge,
_eventSink = eventSink,
_diagnostics = diagnostics;
final RuntimeHostBridge _bridge;
final void Function(RuntimeEvent event) _eventSink;
final RuntimeDiagnostics? _diagnostics;
final Map<String, async.Completer<Object?>> _pendingLuaCalls = {};
var _nextCallId = 0;
bool _disposed = false;
Future<void> callHost(RuntimeHostCall call) async {
if (_disposed) {
return;
}
final handler = _bridge.handlers[call.method];
if (handler == null) {
_emitHostCallResult(
id: call.id,
method: call.method,
ok: false,
error: 'No host handler registered for ${call.method}',
);
return;
}
try {
final result = await handler(call);
_emitHostCallResult(
id: call.id,
method: call.method,
ok: true,
result: result,
);
} catch (error) {
_diagnostics?.record(
type: RuntimeDiagnosticType.hostBridgeError,
message: 'Runtime host call failed',
error: error,
context: {'id': call.id, 'method': call.method},
);
_emitHostCallResult(
id: call.id,
method: call.method,
ok: false,
error: error.toString(),
);
}
}
bool notifyHost(RuntimeHostNotification notification) {
if (_disposed) {
return false;
}
final handler = _bridge.onNotify;
if (handler == null) {
return false;
}
try {
handler(notification);
return true;
} catch (error) {
_diagnostics?.record(
type: RuntimeDiagnosticType.hostBridgeError,
message: 'Runtime host notification failed',
error: error,
context: {'method': notification.method},
);
return false;
}
}
Future<Object?> callLua(
String method, {
Object? data,
Duration timeout = const Duration(seconds: 15),
}) {
if (_disposed) {
return Future<Object?>.error(StateError('Runtime host bridge disposed'));
}
final id = 'host:${++_nextCallId}';
final completer = async.Completer<Object?>();
_pendingLuaCalls[id] = completer;
_emit(
RuntimeEvent(
type: RuntimeHostEventType.call,
data: {
'id': id,
'method': method,
if (data != null) 'data': _runtimeValue(data),
},
),
);
return completer.future.timeout(
timeout,
onTimeout: () {
_pendingLuaCalls.remove(id);
throw async.TimeoutException(
'Lua host call timed out: $method',
timeout,
);
},
);
}
bool notifyLua(String method, {Object? data}) {
if (_disposed) {
return false;
}
_emit(
RuntimeEvent(
type: RuntimeHostEventType.notify,
data: {'method': method, if (data != null) 'data': _runtimeValue(data)},
),
);
return true;
}
bool completeLuaCall(String id, {Object? result, String? error}) {
final completer = _pendingLuaCalls.remove(id);
if (completer == null || completer.isCompleted) {
return false;
}
if (error != null) {
completer.completeError(StateError(error));
} else {
completer.complete(_runtimeValue(result));
}
return true;
}
void dispose() {
_disposed = true;
for (final completer in _pendingLuaCalls.values) {
if (!completer.isCompleted) {
completer.completeError(StateError('Runtime host bridge disposed'));
}
}
_pendingLuaCalls.clear();
}
void _emitHostCallResult({
required String id,
required String method,
required bool ok,
Object? result,
String? error,
}) {
_emit(
RuntimeEvent(
type: RuntimeHostEventType.callResult,
data: {
'id': id,
'method': method,
'ok': ok,
if (ok) 'result': _runtimeValue(result),
if (!ok && error != null) 'error': error,
},
),
);
}
void _emit(RuntimeEvent event) {
if (_disposed) {
return;
}
_eventSink(event);
}
Object? _runtimeValue(Object? value) {
if (value == null || value is String || value is num || value is bool) {
return value;
}
if (value is Iterable) {
return value.map(_runtimeValue).toList(growable: false);
}
if (value is Map) {
return {
for (final entry in value.entries)
entry.key.toString(): _runtimeValue(entry.value),
};
}
return value.toString();
}
}
abstract final class RuntimeHostEventType {
static const notify = 'host_notify';
static const call = 'host_call';
static const callResult = 'host_call_result';
}