diff --git a/packages/dragon_logs/lib/src/storage/opfs_interop.dart b/packages/dragon_logs/lib/src/storage/opfs_interop.dart index 78715d52a..fa291972f 100644 --- a/packages/dragon_logs/lib/src/storage/opfs_interop.dart +++ b/packages/dragon_logs/lib/src/storage/opfs_interop.dart @@ -1,20 +1,21 @@ import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; import 'package:web/web.dart'; /// JavaScript async iterator result type @JS() @anonymous -extension type JSIteratorResult._(JSObject _) implements JSObject { +extension type JSIteratorResult._(JSObject _) + implements JSObject { external bool get done; - external JSAny? get value; + external T? get value; } /// JavaScript async iterator type @JS() @anonymous -extension type JSAsyncIterator._(JSObject _) implements JSObject { - external JSPromise next(); +extension type JSAsyncIterator._(JSObject _) + implements JSObject { + external JSPromise> next(); } /// Extensions for FileSystemDirectoryHandle to provide missing async iterator methods @@ -23,21 +24,21 @@ extension type JSAsyncIterator._(JSObject _) implements JSObject { extension FileSystemDirectoryHandleExtension on FileSystemDirectoryHandle { /// Returns an async iterator for the values (handles) in this directory. /// Equivalent to calling `directoryHandle.values()` in JavaScript. - external JSAsyncIterator values(); + external JSAsyncIterator values(); /// Returns an async iterator for the keys (names) in this directory. /// Equivalent to calling `directoryHandle.keys()` in JavaScript. - external JSAsyncIterator keys(); + external JSAsyncIterator keys(); /// Returns an async iterator for the entries (name-handle pairs) in this directory. /// Equivalent to calling `directoryHandle.entries()` in JavaScript. - external JSAsyncIterator entries(); + external JSAsyncIterator> entries(); } /// Helper extensions to convert JavaScript async iterators to Dart async iterables -extension JSAsyncIteratorExtension on JSAsyncIterator { +extension JSAsyncIteratorExtension on JSAsyncIterator { /// Converts a JavaScript async iterator to a Dart Stream - Stream asStream() async* { + Stream asStream() async* { while (true) { final result = await next().toDart; if (result.done) break; @@ -49,26 +50,37 @@ extension JSAsyncIteratorExtension on JSAsyncIterator { /// Extension to provide async iteration capabilities for FileSystemDirectoryHandle values extension FileSystemDirectoryHandleValuesIterable on FileSystemDirectoryHandle { /// Returns a Stream of FileSystemHandle objects for async iteration over directory contents - Stream valuesStream() { - return values().asStream().map((jsValue) => jsValue as FileSystemHandle); + Stream valuesStream() async* { + await for (final handle in values().asStream()) { + if (handle != null) { + yield handle; + } + } } /// Returns a Stream of file/directory names for async iteration over directory contents - Stream keysStream() { - return keys().asStream().map((jsValue) => (jsValue as JSString).toDart); + Stream keysStream() async* { + await for (final key in keys().asStream()) { + if (key != null) { + yield key.toDart; + } + } } /// Returns a Stream of [name, handle] pairs for async iteration over directory contents - static const int nameIndex = 0; - static const int handleIndex = 1; - Stream<(String, FileSystemHandle)> entriesStream() { - return entries().asStream().map((jsValue) { - // The entries() iterator returns [name, handle] arrays - // Use js_interop_unsafe to access array elements by numeric index - final jsObject = jsValue as JSObject; - final name = jsObject.getProperty(nameIndex.toJS).toDart; - final handle = jsObject.getProperty(handleIndex.toJS); - return (name, handle); - }); + Stream<(String, FileSystemHandle)> entriesStream() async* { + await for (final entry in entries().asStream()) { + if (entry == null || entry.length < 2) { + continue; + } + + final nameValue = entry[0]; + final handleValue = entry[1]; + if (!nameValue.isA() || !handleValue.isA()) { + continue; + } + + yield (nameValue.dartify()! as String, handleValue as FileSystemHandle); + } } } diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 476793f1d..5e8016c17 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -68,7 +68,7 @@ "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "c394bc48ba010966af83500eaae9da45a40d03d3", + "bundled_coins_repo_commit": "e027082339558cc79d653d0e871f0d211562fe2f", "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", "coins_repo_branch": "master", diff --git a/packages/komodo_defi_framework/lib/src/js/js_interop_utils.dart b/packages/komodo_defi_framework/lib/src/js/js_interop_utils.dart index aaf63f3e5..4a82d0cd2 100644 --- a/packages/komodo_defi_framework/lib/src/js/js_interop_utils.dart +++ b/packages/komodo_defi_framework/lib/src/js/js_interop_utils.dart @@ -8,6 +8,11 @@ import 'package:logging/logging.dart'; final Logger _jsInteropLogger = Logger('JsInteropUtils'); +@js_interop.JS('Promise.resolve') +external js_interop.JSPromise _resolveJsPromise( + js_interop.JSAny? value, +); + /// Parses a JS interop response into a JsonMap. /// /// Accepts: @@ -16,14 +21,9 @@ final Logger _jsInteropLogger = Logger('JsInteropUtils'); /// - String (JSON encoded) /// /// Throws a [FormatException] if the response cannot be parsed into a JSON map. -JsonMap parseJsInteropJson(dynamic jsResponse) { +JsonMap parseJsInteropJson(js_interop.JSAny? jsResponse) { try { - dynamic value = jsResponse; - - // If we received a JS value, convert to Dart first - if (value is js_interop.JSAny?) { - value = value?.dartify(); - } + final dynamic value = jsResponse?.dartify(); if (value is String) { final decoded = jsonDecode(value); @@ -45,7 +45,10 @@ JsonMap parseJsInteropJson(dynamic jsResponse) { } /// Generic helper that parses a JS response and maps it to a Dart model. -T parseJsInteropCall(dynamic jsResponse, T Function(JsonMap) fromJson) { +T parseJsInteropCall( + js_interop.JSAny? jsResponse, + T Function(JsonMap) fromJson, +) { final map = parseJsInteropJson(jsResponse); return fromJson(map); } @@ -76,11 +79,12 @@ List _deepConvertList(List list) { /// - If [jsValue] is not a JSPromise, it is dartified directly /// - Returns the dartified dynamic value Future resolveJsAnyMaybePromise(js_interop.JSAny? jsValue) async { - if (jsValue is js_interop.JSPromise) { - final resolved = await jsValue.toDart; - return resolved?.dartify(); + if (jsValue == null || jsValue.isUndefinedOrNull) { + return null; } - return jsValue?.dartify(); + + final resolved = await _resolveJsPromise(jsValue).toDart; + return resolved?.dartify(); } /// Generic helper to resolve a JS interop value (maybe a Promise) and map it. diff --git a/packages/komodo_defi_framework/lib/src/operations/kdf_operations_wasm.dart b/packages/komodo_defi_framework/lib/src/operations/kdf_operations_wasm.dart index 464722889..cc4075217 100644 --- a/packages/komodo_defi_framework/lib/src/operations/kdf_operations_wasm.dart +++ b/packages/komodo_defi_framework/lib/src/operations/kdf_operations_wasm.dart @@ -1,12 +1,10 @@ import 'dart:async'; import 'dart:js_interop' as js_interop; -import 'dart:js_interop_unsafe'; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:http/http.dart'; import 'package:komodo_defi_framework/komodo_defi_framework.dart'; -import 'package:komodo_defi_framework/src/config/kdf_logging_config.dart'; import 'package:komodo_defi_framework/src/js/js_error_utils.dart'; import 'package:komodo_defi_framework/src/js/js_interop_utils.dart'; import 'package:komodo_defi_framework/src/js/js_result_mappers.dart' as js_maps; @@ -16,6 +14,47 @@ import 'package:mutex/mutex.dart'; const _kdfAsstsPath = 'kdf'; const _kdfJsBootstrapperPath = '$_kdfAsstsPath/res/kdflib_bootstrapper.js'; +@js_interop.JS() +extension type _KdfBootstrapperModule._(js_interop.JSObject _) + implements js_interop.JSObject { + @js_interop.JS('default') + external _KdfWasmBindings? get defaultBinding; + + external _KdfWasmBindings? get kdf; +} + +@js_interop.JS() +extension type _KdfWasmBindings._(js_interop.JSObject _) + implements js_interop.JSObject { + external js_interop.JSBoolean get isInitialized; + + @js_interop.JS('init_wasm') + external js_interop.JSPromise? initWasm(); + + @js_interop.JS('mm2_main') + external js_interop.JSAny? mm2Main( + js_interop.JSAny? config, + js_interop.JSFunction logHandler, + ); + + @js_interop.JS('mm2_main_status') + external js_interop.JSNumber? mm2MainStatus(); + + @js_interop.JS('mm2_stop') + external js_interop.JSAny? mm2Stop(); + + @js_interop.JS('mm2_rpc') + external js_interop.JSPromise? mm2Rpc( + js_interop.JSAny? request, + ); +} + +@js_interop.JS() +extension type _KdfErrorWithCode._(js_interop.JSAny _) + implements js_interop.JSAny { + external js_interop.JSAny? get code; +} + IKdfOperations createLocalKdfOperations({ required void Function(String)? logCallback, required LocalConfig config, @@ -40,7 +79,7 @@ class KdfOperationsWasm implements IKdfOperations { final LocalConfig _config; bool _libraryLoaded = false; - js_interop.JSObject? _kdfModule; + _KdfWasmBindings? _kdfModule; void Function(String)? _logger; void _log(String message) => (_logger ?? print).call(message); @@ -62,10 +101,7 @@ class KdfOperationsWasm implements IKdfOperations { } bool get _isWasmInitialized { - return _kdfModule - ?.getProperty('isInitialized'.toJS) - .toDart ?? - false; + return _kdfModule?.isInitialized.toDart ?? false; } @override @@ -80,9 +116,7 @@ class KdfOperationsWasm implements IKdfOperations { return _startupLock.protect(() async { await _ensureLoaded(); - final jsConfig = - {'conf': config, 'log_level': logLevel ?? 3}.jsify() - as js_interop.JSObject?; + final jsConfig = {'conf': config, 'log_level': logLevel ?? 3}.jsify(); try { return await _executeKdfMain(jsConfig); @@ -101,11 +135,8 @@ class KdfOperationsWasm implements IKdfOperations { }); } - Future _executeKdfMain( - js_interop.JSObject? jsConfig, - ) async { - final jsMethod = _kdfModule!.callMethod( - 'mm2_main'.toJS, + Future _executeKdfMain(js_interop.JSAny? jsConfig) async { + final jsMethod = _kdfModule!.mm2Main( jsConfig, (int level, String message) { _log('[$level] KDF: $message'); @@ -122,39 +153,6 @@ class KdfOperationsWasm implements IKdfOperations { try { _debugLog('Handling JSAny error: [${jsError.runtimeType}] $jsError'); - // Direct JSNumber error - if (isInstance(jsError, 'JSNumber')) { - final dynamic dartNumber = (jsError as js_interop.JSNumber).dartify(); - final code = extractNumericCodeFromDartError(dartNumber); - if (code != null) { - _debugLog('KdfOperationsWasm: Resolved as JSNumber code: $code'); - return KdfStartupResult.fromDefaultInt(code); - } - } - - // JSObject with useful fields - if (isInstance(jsError, 'JSObject')) { - final jsObj = jsError as js_interop.JSObject; - - // Prefer robust dartify and then inspect - final dynamic dartified = jsObj.dartify(); - final code = extractNumericCodeFromDartError(dartified); - if (code != null) return KdfStartupResult.fromDefaultInt(code); - - final msg = extractMessageFromDartError(dartified); - if (msg != null && messageIndicatesAlreadyRunning(msg)) { - return KdfStartupResult.alreadyRunning; - } - - // Fallback for 'code' property directly on JS object if not covered above - if (jsObj.hasProperty('code'.toJS).toDart) { - final jsAnyCode = jsObj.getProperty('code'.toJS); - final code2 = extractNumericCodeFromDartError(jsAnyCode?.dartify()); - if (code2 != null) return KdfStartupResult.fromDefaultInt(code2); - } - } - - // Try dartify as last resort final dynamic error = jsError.dartify(); _debugLog('Dartified error type: ${error.runtimeType}, value: $error'); @@ -166,6 +164,12 @@ class KdfOperationsWasm implements IKdfOperations { return KdfStartupResult.alreadyRunning; } + final codeValue = _KdfErrorWithCode._(jsError).code?.dartify(); + final codeFromProperty = extractNumericCodeFromDartError(codeValue); + if (codeFromProperty != null) { + return KdfStartupResult.fromDefaultInt(codeFromProperty); + } + _log('Could not extract error code from JSAny: $error'); } catch (conversionError) { _log('Error during JSAny conversion: $conversionError'); @@ -174,19 +178,10 @@ class KdfOperationsWasm implements IKdfOperations { return KdfStartupResult.unknownError; } - bool isInstance( - js_interop.JSAny? obj, [ - String? typeString, - ]) { - return obj.instanceOfString(typeString ?? T.runtimeType.toString()); - } - @override Future kdfMainStatus() async { await _ensureLoaded(); - final status = _kdfModule! - .callMethod('mm2_main_status'.toJS) - ?.toDartInt; + final status = _kdfModule!.mm2MainStatus()?.toDartInt; return MainStatus.fromDefaultInt(status!); } @@ -196,13 +191,14 @@ class KdfOperationsWasm implements IKdfOperations { try { // Call mm2_stop which may return a Promise or a direct value - final jsAny = _kdfModule!.callMethod('mm2_stop'.toJS); + final jsAny = _kdfModule!.mm2Stop(); final status = await parseJsInteropMaybePromise( jsAny, js_maps.mapJsStopResult, ); - // Ensure the node actually stops when we expect success or already stopped + // Ensure the node actually stops when we expect success or an + // already-stopped result. if (status == StopStatus.ok || status == StopStatus.stoppingAlready) { await Future.doWhile(() async { final isStopped = (await kdfMainStatus()) == MainStatus.notRunning; @@ -237,14 +233,12 @@ class KdfOperationsWasm implements IKdfOperations { } /// Makes the JavaScript RPC call and returns the raw JS response - Future _makeJsCall(JsonMap request) async { + Future _makeJsCall(JsonMap request) async { _debugLog('mm2Rpc request: ${request.censored()}'); request['userpass'] = _config.rpcPassword; - final jsRequest = request.jsify() as js_interop.JSObject?; - final jsPromise = - _kdfModule!.callMethod('mm2_rpc'.toJS, jsRequest) - as js_interop.JSPromise?; + final jsRequest = request.jsify(); + final jsPromise = _kdfModule!.mm2Rpc(jsRequest); if (jsPromise == null || jsPromise.isUndefinedOrNull) { throw Exception( @@ -281,14 +275,14 @@ class KdfOperationsWasm implements IKdfOperations { } catch (e) { _debugLog('Raw JS response: $jsResponse (stringify failed: $e)'); } - return jsResponse as js_interop.JSObject; + return jsResponse; } /// Validates the response structure void _validateResponse( JsonMap dartResponse, JsonMap request, - js_interop.JSObject jsResponse, + js_interop.JSAny? jsResponse, ) { // Legacy RPCs have no standard response format to validate if (request.valueOrNull('mmrpc') != '2.0') return; @@ -327,7 +321,7 @@ class KdfOperationsWasm implements IKdfOperations { } bool _areFunctionsLoaded() { - return _kdfModule?.hasProperty('mm2_main'.toJS).toDart ?? false; + return _kdfModule != null; } Future _ensureLoaded() async { @@ -349,8 +343,7 @@ class KdfOperationsWasm implements IKdfOperations { } Future _initWasm() async { - final initWasmPromise = - _kdfModule?.callMethod('init_wasm'.toJS) as js_interop.JSPromise?; + final initWasmPromise = _kdfModule?.initWasm(); if (initWasmPromise != null) { await initWasmPromise.toDart; } @@ -358,11 +351,14 @@ class KdfOperationsWasm implements IKdfOperations { Future _injectLibrary() async { try { - _kdfModule = - (await js_interop - .importModule('./$_kdfJsBootstrapperPath'.toJS) - .toDart) - .getProperty('kdf'.toJS); + final module = _KdfBootstrapperModule._( + await js_interop.importModule('./$_kdfJsBootstrapperPath'.toJS).toDart, + ); + _kdfModule = module.kdf ?? module.defaultBinding; + + if (_kdfModule == null) { + throw StateError('Imported KDF module did not expose a kdf binding.'); + } _log('KDF library loaded successfully'); } catch (e) { @@ -370,29 +366,6 @@ class KdfOperationsWasm implements IKdfOperations { 'Failed to load and import script $_kdfJsBootstrapperPath\n$e'; _log(message); - final debugProperties = Map.fromIterable( - [ - 'isInitialized', - 'kdf', - 'initSync', - 'initWasm', - 'init', - 'mm2_main', - 'mm2_main_status', - 'mm2_stop', - 'mm2_init', - 'init_wasm', - '__wbg_init', - ], - value: (key) { - final jsKey = (key as String).toJS; - return 'Has property: ${_kdfModule!.hasProperty(jsKey).toDart} with type: ' - '${_kdfModule!.getProperty(jsKey).runtimeType}'; - }, - ); - - _log('KDF Has properties: $debugProperties'); - throw Exception(message); } } @@ -413,13 +386,12 @@ class KdfOperationsWasm implements IKdfOperations { class KdfPluginWeb { static void registerWith(Registrar registrar) { - final channel = MethodChannel( + MethodChannel( 'komodo_defi_framework', const StandardMethodCodec(), registrar, - ); - channel.setMethodCallHandler((call) async { - // Handle method calls here if needed + ).setMethodCallHandler((call) async { + // Handle method calls here if needed. }); registrar.registerMessageHandler(); diff --git a/packages/komodo_defi_framework/lib/src/streaming/event_streaming_platform_web.dart b/packages/komodo_defi_framework/lib/src/streaming/event_streaming_platform_web.dart index 4aa78e0a8..3bef535f4 100644 --- a/packages/komodo_defi_framework/lib/src/streaming/event_streaming_platform_web.dart +++ b/packages/komodo_defi_framework/lib/src/streaming/event_streaming_platform_web.dart @@ -2,34 +2,31 @@ // and forward messages to Dart via the provided callback. import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; import 'package:flutter/foundation.dart'; -import 'package:web/web.dart' as web; - import 'package:komodo_defi_framework/src/config/kdf_config.dart'; +import 'package:web/web.dart' as web; typedef EventStreamUnsubscribe = void Function(); const _eventStreamingWorkerPath = 'assets/packages/komodo_defi_framework/assets/web/event_streaming_worker.js'; -final web.EventHandler _noopHandler = ((web.Event _) {}).toJS; +final web.EventHandlerNonNull _noopHandler = ((web.Event _) {}).toJS; EventStreamUnsubscribe connectEventStream({ - IKdfHostConfig? hostConfig, required void Function(Object? data) onMessage, required void Function() onFirstByte, + IKdfHostConfig? hostConfig, }) { try { final worker = web.SharedWorker(_eventStreamingWorkerPath.toJS); - final port = worker.port; - port.start(); + final port = worker.port..start(); bool firstMessageReceived = false; - void handler(web.Event event) { - final data = event is web.MessageEvent ? event.data.dartify() : null; + void handler(web.MessageEvent event) { + final data = event.data.dartify(); // Signal first byte received on first message if (!firstMessageReceived) { @@ -47,8 +44,9 @@ EventStreamUnsubscribe connectEventStream({ return () { try { - port.onmessage = _noopHandler; - port.close(); + port + ..onmessage = _noopHandler + ..close(); } catch (_) {} }; } catch (_) { diff --git a/packages/komodo_defi_framework/web/res/kdf_wrapper.dart b/packages/komodo_defi_framework/web/res/kdf_wrapper.dart index 46dc6b661..c39c9fe47 100644 --- a/packages/komodo_defi_framework/web/res/kdf_wrapper.dart +++ b/packages/komodo_defi_framework/web/res/kdf_wrapper.dart @@ -8,12 +8,20 @@ import 'dart:async'; // This is a web-specific file, so it's safe to ignore this warning // ignore: avoid_web_libraries_in_flutter import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:web/web.dart'; +@JS('mm2_main') +external JSAny? _mm2MainJs(String conf, JSFunction logCallback); + +@JS('mm2_main_status') +external JSAny? _mm2MainStatusJs(); + +@JS('mm2_stop') +external JSAny? _mm2StopJs(); + class KdfPlugin { static void registerWith(Registrar registrar) { final plugin = KdfPlugin(); @@ -62,7 +70,7 @@ class KdfPlugin { final completer = Completer(); - final script = (document.createElement('script') as HTMLScriptElement) + final script = HTMLScriptElement() ..src = 'kdf/kdflib.js' ..onload = () { _libraryLoaded = true; @@ -82,15 +90,12 @@ class KdfPlugin { try { final jsCallback = logCallback.toJS; - final jsResponse = globalContext.callMethod( - 'mm2_main'.toJS, - [conf.toJS, jsCallback].toJS, - ); + final jsResponse = _mm2MainJs(conf, jsCallback); if (jsResponse == null) { throw Exception('mm2_main call returned null'); } - final dynamic dartResponse = (jsResponse as JSAny?).dartify(); + final dynamic dartResponse = jsResponse.dartify(); if (dartResponse == null) { throw Exception('Failed to convert mm2_main response to Dart'); } @@ -106,13 +111,13 @@ class KdfPlugin { throw StateError('KDF library not loaded. Call ensureLoaded() first.'); } - final jsResult = globalContext.callMethod('mm2_main_status'.toJS); + final jsResult = _mm2MainStatusJs(); return jsResult.dartify()! as int; } Future _mm2Stop() async { await _ensureLoaded(); - final jsResult = globalContext.callMethod('mm2_stop'.toJS); + final jsResult = _mm2StopJs(); return jsResult.dartify()! as int; } }