Add bidirectional host bridge
This commit is contained in:
228
lib/runtime/host/runtime_host_bridge.dart
Normal file
228
lib/runtime/host/runtime_host_bridge.dart
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user