229 lines
5.6 KiB
Dart
229 lines
5.6 KiB
Dart
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';
|
|
}
|