diff --git a/.editorconfig b/.editorconfig index c602028..ba5b634 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,3 +11,9 @@ trim_trailing_whitespace = true # Markdown files can have flexible indentation for lists [*.md] indent_size = unset + +[.mise.toml] +max_line_length = off + +[*.dart] +max_line_length = off diff --git a/.mise.toml b/.mise.toml index 3f6416f..fac192a 100644 --- a/.mise.toml +++ b/.mise.toml @@ -5,6 +5,7 @@ action-validator = "latest" "chromedriver" = "146" cmake = "latest" codex = "latest" +dart = { version = "latest", url = "https://storage.googleapis.com/dart-archive/channels/stable/release/{{ version }}/sdk/dartsdk-{{ os() }}-{{ arch() }}-release.zip", version_expr = 'fromJSON(body).prefixes | filter({ # matches "^channels/stable/release/(\\d+\\.\\d+\\.\\d+)/$" }) | map({split(#, "/")[3]}) | sortVersions()', version_list_url = "https://storage.googleapis.com/storage/v1/b/dart-archive/o?prefix=channels/stable/release/&delimiter=/" } dprint = "latest" editorconfig-checker = "latest" gemini-cli = "latest" @@ -22,6 +23,7 @@ typos = "latest" uv = "latest" [tasks] +ec = "ec" editorconfig-check = "ec" ruff-check = "ruff check services/ws-modules/" ruff-fmt = "ruff format services/ws-modules/" @@ -36,17 +38,24 @@ DPRINT_CACHE_DIR = "/tmp/dprint-cache" run = "dprint fmt" [tasks.fmt] -depends = ["cargo-clippy-fix", "cargo-fmt", "dprint-fmt", "ruff-fmt", "taplo-fmt"] +depends = ["cargo-clippy-fix", "cargo-fmt", "dart-fmt", "dprint-fmt", "ruff-fmt", "taplo-fmt"] description = "Run repository formatters" [tasks.install-nightly] run = "cargo +nightly fmt --version >/dev/null 2>&1 || rustup toolchain install nightly --component rustfmt" +[tasks.dart-check] +run = "dart analyze services/ws-modules/dart-comm1/" + +[tasks.dart-fmt] +run = "dart format services/ws-modules/dart-comm1/" + [tasks.check] depends = [ "cargo-check", "cargo-clippy", "cargo-fmt-check", + "dart-check", "dprint-check", "editorconfig-check", "osv-scanner", @@ -164,6 +173,11 @@ description = "Build the nfc workflow WASM module" dir = "services/ws-modules/nfc" run = "wasm-pack build . --target web" +[tasks.build-ws-dart-comm1-module] +description = "Build the dart-comm1 workflow module" +dir = "services/ws-modules/dart-comm1" +run = "dart compile js lib/dart_comm1.dart -o pkg/et_ws_dart_comm1_compiled.js --no-source-maps" + [tasks.build-ws-pydata1-module] description = "Build the pydata1 Python workflow module" dir = "services/ws-modules/pydata1" @@ -177,6 +191,7 @@ depends = [ "build-ws-audio1-module", "build-ws-bluetooth-module", "build-ws-comm1-module", + "build-ws-dart-comm1-module", "build-ws-data1-module", "build-ws-face-detection-module", "build-ws-geolocation-module", diff --git a/services/ws-modules/dart-comm1/.gitignore b/services/ws-modules/dart-comm1/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/services/ws-modules/dart-comm1/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/services/ws-modules/dart-comm1/lib/dart_comm1.dart b/services/ws-modules/dart-comm1/lib/dart_comm1.dart new file mode 100644 index 0000000..a3c9b28 --- /dev/null +++ b/services/ws-modules/dart-comm1/lib/dart_comm1.dart @@ -0,0 +1,155 @@ +import 'dart:async'; +import 'dart:js_interop'; + +// JS interop declarations for et_ws_wasm_agent +@JS() +extension type WsClientConfig._(JSObject _) implements JSObject { + external factory WsClientConfig(String serverUrl); +} + +@JS() +extension type WsClient._(JSObject _) implements JSObject { + external factory WsClient(WsClientConfig config); + external void connect(); + external void disconnect(); + // ignore: non_constant_identifier_names + external String get_state(); + // ignore: non_constant_identifier_names + external String get_client_id(); + external void send(String message); + // ignore: non_constant_identifier_names + external void set_on_message(JSFunction callback); +} + +// JS interop for browser globals +@JS('window.location.protocol') +external String get locationProtocol; + +@JS('window.location.host') +external String get locationHost; + +@JS('document.getElementById') +external JSObject? getElementById(String id); + +@JS() +extension type _TextArea._(JSObject _) implements JSObject { + external String get value; + external set value(String v); +} + +void appendOutput(String msg) { + final el = getElementById('module-output'); + if (el != null) { + final ta = el as _TextArea; + ta.value = ta.value.isEmpty ? msg : '${ta.value}\n$msg'; + } +} + +void log(String msg) { + appendOutput('[dart-comm1] $msg'); +} + +String get wsUrl { + final proto = locationProtocol == 'https:' ? 'wss:' : 'ws:'; + return '$proto//$locationHost/ws'; +} + +Future sleep(int ms) { + final c = Completer(); + Timer(Duration(milliseconds: ms), c.complete); + return c.future; +} + +Future waitForConnected(WsClient client) async { + for (var i = 0; i < 100; i++) { + if (client.get_state() == 'connected') return; + await sleep(100); + } + throw Exception('Timeout waiting for WebSocket connection'); +} + +Future waitForAgentId(WsClient client) async { + for (var i = 0; i < 100; i++) { + final id = client.get_client_id(); + if (id.isNotEmpty) return id; + await sleep(100); + } + throw Exception('Timeout waiting for agent_id'); +} + +Future run() async { + log('entered run()'); + + final client = WsClient(WsClientConfig(wsUrl)); + + String selfAgentId = ''; + String? targetAgentId; + + client.set_on_message( + ((JSString raw) { + final data = raw.toDart; + try { + // Parse type field manually to avoid a JSON dep + if (data.contains('"list_agents_response"')) { + // Extract first other connected agent id + final idMatches = RegExp( + r'"agent_id"\s*:\s*"([^"]+)"', + ).allMatches(data); + for (final m in idMatches) { + final id = m.group(1)!; + if (id != selfAgentId) { + targetAgentId = id; + break; + } + } + } else if (data.contains('"agent_message"') || + data.contains('"message_status"')) { + log('received: $data'); + appendOutput(data); + } + } catch (_) {} + }).toJS, + ); + + client.connect(); + await waitForConnected(client); + selfAgentId = await waitForAgentId(client); + log('connected as $selfAgentId'); + + // Poll for a peer agent + while (targetAgentId == null) { + client.send('{"type":"list_agents"}'); + await sleep(1000); + } + + log('found peer $targetAgentId, sending broadcast'); + client.send( + '{"type":"broadcast_message","message":{"module":"dart-comm1","step":"broadcast","from_agent_id":"$selfAgentId","message":"dart-comm1 broadcast to all other connected agents"}}', + ); + + await sleep(3000); + + log('sending direct message to $targetAgentId'); + client.send( + '{"type":"send_agent_message","to_agent_id":"$targetAgentId","message":{"module":"dart-comm1","step":"direct","from_agent_id":"$selfAgentId","message":"dart-comm1 direct message"}}', + ); + + await sleep(3000); + client.disconnect(); + log('workflow complete'); +} + +@JS('dartComm1Run') +external set _dartComm1Run(JSFunction f); + +void main() { + _dartComm1Run = (() { + return (() async { + try { + await run(); + } catch (e, st) { + throw '$e\n$st'.toJS; + } + }().toJS); + }.toJS); +} diff --git a/services/ws-modules/dart-comm1/pkg/.gitignore b/services/ws-modules/dart-comm1/pkg/.gitignore new file mode 100644 index 0000000..50ebc2b --- /dev/null +++ b/services/ws-modules/dart-comm1/pkg/.gitignore @@ -0,0 +1 @@ +*_compiled* diff --git a/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js b/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js new file mode 100644 index 0000000..d6bf2c5 --- /dev/null +++ b/services/ws-modules/dart-comm1/pkg/et_ws_dart_comm1.js @@ -0,0 +1,34 @@ +// et_ws_dart_comm1.js — ES module shim for dart-comm1 + +export default async function init() { + await new Promise((resolve, reject) => { + const s = document.createElement("script"); + s.src = new URL("et_ws_dart_comm1_compiled.js", import.meta.url).href; + s.onload = resolve; + s.onerror = reject; + document.head.appendChild(s); + }); +} + +export async function run() { + if (typeof globalThis.dartComm1Run !== "function") { + throw new Error("dart-comm1: not initialized"); + } + // Dart @JS() interop resolves against globalThis, so expose the wasm-agent + // classes there for the duration of the call. + const { WsClient, WsClientConfig } = await import("/pkg/et_ws_wasm_agent.js"); + globalThis.WsClient = WsClient; + globalThis.WsClientConfig = WsClientConfig; + try { + const result = globalThis.dartComm1Run(); + console.log("dart-comm1 dartComm1Run returned:", result, typeof result); + await result; + } catch (e) { + console.error("dart-comm1 raw error:", e, "boxed:", e?.error); + const msg = e?.error?.toString?.() ?? e?.message ?? String(e); + throw new Error(msg); + } finally { + delete globalThis.WsClient; + delete globalThis.WsClientConfig; + } +} diff --git a/services/ws-modules/dart-comm1/pkg/package.json b/services/ws-modules/dart-comm1/pkg/package.json new file mode 100644 index 0000000..23b0bef --- /dev/null +++ b/services/ws-modules/dart-comm1/pkg/package.json @@ -0,0 +1,8 @@ +{ + "name": "et-ws-dart-comm1", + "type": "module", + "description": "dart comm1", + "version": "0.1.0", + "license": "Apache-2.0 OR MIT", + "main": "et_ws_dart_comm1.js" +} diff --git a/services/ws-modules/dart-comm1/pubspec.yaml b/services/ws-modules/dart-comm1/pubspec.yaml new file mode 100644 index 0000000..c7d3aef --- /dev/null +++ b/services/ws-modules/dart-comm1/pubspec.yaml @@ -0,0 +1,13 @@ +name: et_dart_comm1 +description: dart-comm1 workflow module +version: 0.1.0 +repository: https://github.com/edge-toolkit/core + +environment: + sdk: ^3.11.5 + +dependencies: + web: ^1.1.0 + +dev_dependencies: + lints: ^6.0.0