From 45c1ee294971011aab54d30f612d445edb35e6ed Mon Sep 17 00:00:00 2001 From: Danylo Silin Date: 2025年7月12日 17:31:31 +0300 Subject: [PATCH] Implemented `talker` package; Migrated from `dart:js` to `dart:js_interop`; Added timeoutDuration parameter in compute method; Package supports older dart versions --- bin/flutter_node_worker.dart | 340 +++++++++++++++++-------------- bin/utils.dart | 129 ++++++------ example/lib/pages/home_page.dart | 8 +- example/pubspec.lock | 26 ++- example/talker/.gitignore | 24 +++ example/talker/index.html | 101 +++++++++ example/talker/package.json | 14 ++ example/talker/public/vite.svg | 1 + example/talker/src/main.js | 14 ++ example/talker/src/style.css | 96 +++++++++ example/talker/src/worker.js | 53 +++++ example/talker/vite.config.js | 52 +++++ example/web/firebase_config.js | 17 ++ lib/flutter_node_worker.dart | 80 ++++---- lib/message_event.dart | 9 + lib/worker.dart | 13 +- lib/worker_options.dart | 3 +- node_modules/.package-lock.json | 2 +- package-lock.json | 2 +- pubspec.yaml | 7 +- 20 files changed, 728 insertions(+), 263 deletions(-) create mode 100644 example/talker/.gitignore create mode 100644 example/talker/index.html create mode 100644 example/talker/package.json create mode 100644 example/talker/public/vite.svg create mode 100644 example/talker/src/main.js create mode 100644 example/talker/src/style.css create mode 100644 example/talker/src/worker.js create mode 100644 example/talker/vite.config.js create mode 100644 example/web/firebase_config.js create mode 100644 lib/message_event.dart diff --git a/bin/flutter_node_worker.dart b/bin/flutter_node_worker.dart index 5ab74f7..71bbf63 100644 --- a/bin/flutter_node_worker.dart +++ b/bin/flutter_node_worker.dart @@ -1,8 +1,11 @@ import "dart:io"; import "package:args/args.dart"; import 'package:path/path.dart' as p; +import 'package:talker/talker.dart'; import 'utils.dart'; +final logger = Talker(); + Future main(List arguments) async { const List commands = [ "init", @@ -25,52 +28,55 @@ Future main(List arguments) async { parser.addCommand(command); } options.forEach( - (key, value) => - value.isNotEmpty - ? parser.addOption( - key, - abbr: value["abbr"], - defaultsTo: value["defaultsTo"], - ) - : parser.addOption(key), + (key, value) => value.isNotEmpty + ? parser.addOption( + key, + abbr: value["abbr"], + defaultsTo: value["defaultsTo"], + ) + : parser.addOption(key), ); final ArgResults results = parser.parse(arguments); final ArgResults? command = results.command; - if (command == null) { - print("Usage: fmw [options]"); - print("Commands: $commands"); - exit(0); + if (arguments.isEmpty) { + logger.info("Usage: dart run flutter_node_worker [options]"); + logger.info("Commands: $commands"); + exit(1); + } else if (command == null) { + logger.error("Unknown command: '${arguments.first}'"); + exit(1); } - final ArgResults initArgs = parser.parse(command.arguments); + + final ArgResults parsedCommandArgs = parser.parse(command.arguments); switch (command.name) { case "init": - await _handleInit(initArgs); + await _handleInit(parsedCommandArgs); break; case "build": - _handleBuildAll(initArgs); + _handleBuildAll(parsedCommandArgs); break; case "add": - _handleAdd(initArgs); + _handleAdd(parsedCommandArgs); break; case "install": - _handleInstall(initArgs); + _handleInstall(parsedCommandArgs); break; case "uninstall": - _handleUninstall(initArgs); + _handleUninstall(parsedCommandArgs); break; case "run": - _handleRun(initArgs); + _handleRun(parsedCommandArgs); break; default: - print("Unknown command: ${command.name}"); + logger.log("Unknown command: ${command.name}"); } } Future _handleInit(ArgResults initArgs) async { - print("Initializing module worker..."); + logger.info("Initializing module worker..."); final String dir = initArgs["dir"] as String; final String name = initArgs["name"] as String; @@ -81,14 +87,17 @@ Future _handleInit(ArgResults initArgs) async { targetDir.createSync(recursive: true); } - Process.runSync("npm", [ - "create", - "vite@latest", - dir, - "--", - "--template", - "vanilla", - ], runInShell: true); + Process.runSync( + "npm", + [ + "create", + "vite@latest", + dir, + "--", + "--template", + "vanilla", + ], + runInShell: true); // Process.runSync("npm", ["install", "vite", "-D"], runInShell: true); Process.runSync("npm", ["install"], runInShell: true); @@ -104,178 +113,193 @@ Future _handleInit(ArgResults initArgs) async { Utils.clearDirectory(dir); - print("The module worker was initialized successfully."); + logger.info("The module worker was initialized successfully."); } void _handleBuildAll(ArgResults initArgs) { - final String dir = initArgs["dir"] as String; - final String outPath = initArgs["out-dir"] as String; - - print("🔧 Auto-building all workers in '$dir/src/' using Vite..."); + try { + final String dir = initArgs["dir"] as String; + final String outPath = initArgs["out-dir"] as String; - final srcDir = Directory(p.join(dir, "src")); - if (!srcDir.existsSync()) { - throw Exception("❌ No src directory found at $dir/src/"); - } - - final outDir = Directory(outPath); - if (!outDir.existsSync()) { - Directory(outPath).createSync(recursive: true); - } - - final viteConfig = File(p.join(dir, "vite.config.js")); - if (!viteConfig.existsSync()) { - throw Exception("❌ Missing vite.config.js in $dir/"); - } + logger.info("🔧 Auto-building all workers in '$dir/src/' using Vite..."); - final entryFiles = - srcDir - .listSync() - .whereType() - .where( - (f) => - f.path.endsWith(".ts") || - f.path.endsWith(".js") && !f.path.contains("main.js"), - ) - .toList(); - - if (entryFiles.isEmpty) { - print("⚠️ No .ts or .js files found in src/"); - return; - } - - for (final file in entryFiles) { - final fileName = p.basename(file.path); - final entryPath = p.relative(file.path, from: dir); - final outputName = "${p.basenameWithoutExtension(fileName)}_module.js"; + final srcDir = Directory(p.join(dir, "src")); + if (!srcDir.existsSync()) { + throw Exception("❌ No src directory found at $dir/src/"); + } - print("📦 Building: $fileName → dist/$outputName.js"); + final outDir = Directory(outPath); + if (!outDir.existsSync()) { + Directory(outPath).createSync(recursive: true); + } - final result = Process.runSync( - "npm", - ["run", "build"], - workingDirectory: dir, - runInShell: true, - environment: { - "ENTRY": entryPath, - "FILENAME": outputName, - "OUTDIR": outPath, - }, - ); + final viteConfig = File(p.join(dir, "vite.config.js")); + if (!viteConfig.existsSync()) { + throw Exception("❌ Missing vite.config.js in $dir/"); + } - stdout.write(result.stdout); - stderr.write(result.stderr); + final entryFiles = srcDir + .listSync() + .whereType() + .where( + (f) => + f.path.endsWith(".ts") || + f.path.endsWith(".js") && !f.path.contains("main.js"), + ) + .toList(); + + if (entryFiles.isEmpty) { + logger.error("⚠️ No .js files found in src/"); + return; + } - if (result.exitCode != 0) { - print("❌ Failed to build $fileName"); - } else { - print("✅ Built $outputName.js"); + for (final file in entryFiles) { + final fileName = p.basename(file.path); + final entryPath = p.relative(file.path, from: dir); + final outputName = "${p.basenameWithoutExtension(fileName)}_module.js"; + + logger.info("📦 Building: $fileName → dist/$outputName.js"); + + final result = Process.runSync( + "npm", + ["run", "build"], + workingDirectory: dir, + runInShell: true, + environment: { + "ENTRY": entryPath, + "FILENAME": outputName, + "OUTDIR": outPath, + }, + ); + + stdout.write(result.stdout); + stderr.write(result.stderr); + + if (result.exitCode != 0) { + logger.error("❌ Failed to build $fileName"); + } else { + logger.info("✅ Built $outputName.js"); + } } - } - print("🎉 All workers built!"); + logger.info("🎉 All workers built!"); + } catch (e, st) { + logger.handle(e, st); + } } void _handleAdd(ArgResults initArgs) async { - print("Adding new worker..."); + try { + logger.info("Adding new worker..."); - final String dir = initArgs["dir"] as String; - final String name = initArgs["name"] as String; + final String dir = initArgs["dir"] as String; + final String name = initArgs["name"] as String; - final Directory targetDir = Directory(dir); - if (!targetDir.existsSync()) { - throw Exception("No such derectory: $dir"); - } + final Directory targetDir = Directory(dir); + if (!targetDir.existsSync()) { + throw Exception("No such derectory: $dir"); + } - final resolvedPath = await Utils.resolveTemplatePath('vite/src/worker.js'); + final resolvedPath = await Utils.resolveTemplatePath('vite/src/worker.js'); - Utils.renderTemplateFile( - inputPath: resolvedPath, - outputPath: '$dir/src/$name.js', - variables: {'workerName': name}, - ); + Utils.renderTemplateFile( + inputPath: resolvedPath, + outputPath: '$dir/src/$name.js', + variables: {'workerName': name}, + ); - print("Added worker to $dir"); + logger.info("Added worker to $dir"); + } catch (e, st) { + logger.handle(e, st); + } } void _handleInstall(ArgResults initArgs) { - print("Installing npm packages..."); + try { + logger.info("Installing npm packages..."); - final String dir = initArgs["dir"] as String; + final String dir = initArgs["dir"] as String; - final Directory targetDir = Directory(dir); - if (!targetDir.existsSync()) { - throw Exception("No such derectory: $dir"); - } + final Directory targetDir = Directory(dir); + if (!targetDir.existsSync()) { + throw Exception("No such derectory: $dir"); + } - final List packages = initArgs.arguments.toList(); - packages.removeWhere((arg) => arg == "--dir" || arg == dir); + final List packages = initArgs.arguments.toList(); + packages.removeWhere((arg) => arg == "--dir" || arg == dir); - final result = Process.runSync( - "npm", - ["install", ...packages], - workingDirectory: dir, - runInShell: true, - ); + final result = Process.runSync( + "npm", + ["install", ...packages], + workingDirectory: dir, + runInShell: true, + ); - if (result.exitCode != 0) { - print("❌ Failed to install packages"); - } else { - print("✅ Installed packages"); - } + if (result.exitCode != 0) { + logger.error("❌ Failed to install packages"); + } - print("🎉 All packages installed!"); + logger.info("🎉 All packages installed!"); + } catch (e, st) { + logger.handle(e, st); + } } void _handleUninstall(ArgResults initArgs) { - print("Uninstalling npm packages..."); + try { + logger.info("Uninstalling npm packages..."); - final String dir = initArgs["dir"] as String; + final String dir = initArgs["dir"] as String; - final Directory targetDir = Directory(dir); - if (!targetDir.existsSync()) { - throw Exception("No such derectory: $dir"); - } + final Directory targetDir = Directory(dir); + if (!targetDir.existsSync()) { + throw Exception("No such derectory: $dir"); + } - final List packages = initArgs.arguments.toList(); - packages.removeWhere((arg) => arg == "--dir" || arg == dir); + final List packages = initArgs.arguments.toList(); + packages.removeWhere((arg) => arg == "--dir" || arg == dir); - final result = Process.runSync( - "npm", - ["uninstall", ...packages], - workingDirectory: dir, - runInShell: true, - ); + final result = Process.runSync( + "npm", + ["uninstall", ...packages], + workingDirectory: dir, + runInShell: true, + ); - if (result.exitCode != 0) { - print("❌ Failed to uninstall packages"); - } else { - print("✅ Uninstalled packages"); - } + if (result.exitCode != 0) { + logger.error("❌ Failed to uninstall packages"); + } - print("🎉 All packages uninstalled!"); + logger.info("🎉 All packages uninstalled!"); + } catch (e, st) { + logger.handle(e, st); + } } void _handleRun(ArgResults initArgs) { - print("Running vite project..."); + try { + logger.info("Running vite project..."); - final String dir = initArgs["dir"] as String; + final String dir = initArgs["dir"] as String; - final Directory targetDir = Directory(dir); - if (!targetDir.existsSync()) { - throw Exception("No such derectory: $dir"); - } + final Directory targetDir = Directory(dir); + if (!targetDir.existsSync()) { + throw Exception("No such derectory: $dir"); + } - final result = Process.runSync( - "npm", - ["run", "dev"], - workingDirectory: dir, - runInShell: true, - ); + final result = Process.runSync( + "npm", + ["run", "dev"], + workingDirectory: dir, + runInShell: true, + ); - if (result.exitCode != 0) { - print("❌ Failed to uninstall packages"); - } else { - print("✅ Uninstalled packages"); + if (result.exitCode != 0) { + logger.error("❌ Failed to uninstall packages"); + } else { + logger.info("✅ Uninstalled packages"); + } + } catch (e, st) { + logger.handle(e, st); } } diff --git a/bin/utils.dart b/bin/utils.dart index 7a0e815..e9cdad9 100644 --- a/bin/utils.dart +++ b/bin/utils.dart @@ -3,27 +3,36 @@ import "dart:isolate"; import "package:mustache_template/mustache.dart"; import "package:path/path.dart" as p; +import "package:talker/talker.dart"; import "constants.dart"; +final Talker logger = Talker(); + class Utils { static Future resolveTemplatePath(String relativePath) async { - final uri = Uri.parse("package:$packageName/templates/$relativePath"); - final resolved = await Isolate.resolvePackageUri(uri); - - if (resolved == null) { - // Fallback для локальной разработки - final scriptDir = File.fromUri(Platform.script).parent; - final rootDir = - scriptDir.path.contains('/example/') - ? Directory(p.normalize(p.join(scriptDir.path, '../'))).absolute - : Directory.current; - final fullPath = p.join(rootDir.path, 'templates', relativePath); - if (!File(fullPath).existsSync()) { - throw Exception("Template not found: $fullPath"); + try { + final uri = Uri.parse("package:$packageName/templates/$relativePath"); + final resolved = await Isolate.resolvePackageUri(uri); + + if (resolved == null) { + // Fallback для локальной разработки + final scriptDir = File.fromUri(Platform.script).parent; + final rootDir = scriptDir.path.contains('/example/') + ? Directory(p.normalize(p.join(scriptDir.path, '../'))).absolute + : Directory.current; + final fullPath = p.join(rootDir.path, 'templates', relativePath); + + if (!File(fullPath).existsSync()) { + throw Exception("Template not found: $fullPath"); + } + + return fullPath; } - return fullPath; + return resolved.toFilePath(windows: Platform.isWindows); + } catch (e, st) { + logger.handle(e, st); + throw Exception("Failed to resolve template path"); } - return resolved.toFilePath(windows: Platform.isWindows); } static void renderTemplateFile({ @@ -43,18 +52,18 @@ class Utils { ); final choice = stdin.readLineSync()?.trim().toUpperCase(); if (choice == "S") { - print("Skipped: $outputPath"); + logger.info("Skipped: $outputPath"); return; } else if (choice == "A") { outputFile.writeAsStringSync("\n$rendered", mode: FileMode.append); - print("Added to the end: $outputPath"); + logger.info("Added to the end: $outputPath"); return; } else if (choice == "O" || choice == null || choice.isEmpty) { outputFile.writeAsStringSync(rendered); - print("File overwritten: $outputPath"); + logger.info("File overwritten: $outputPath"); return; } else { - print("Unknown choice, skipped: $outputPath"); + logger.info("Unknown choice, skipped: $outputPath"); return; } } else { @@ -70,54 +79,60 @@ class Utils { required String targetWorkerDir, required Map vars, }) async { - final srcDir = Directory(from); - final dstDir = Directory(to); + try { + final srcDir = Directory(from); + final dstDir = Directory(to); - if (!srcDir.existsSync()) { - throw Exception("The templates was not found: $from"); - } + if (!srcDir.existsSync()) { + throw Exception("The templates was not found: $from"); + } - for (var entity in srcDir.listSync(recursive: true)) { - if (entity is File) { - // String relativePath = entity.path.substring(from.length + 1); - String relativePath = p.relative(entity.path, from: srcDir.path); + for (var entity in srcDir.listSync(recursive: true)) { + if (entity is File) { + // String relativePath = entity.path.substring(from.length + 1); + String relativePath = p.relative(entity.path, from: srcDir.path); - // final isTemplate = relativePath.endsWith(".template"); + // final isTemplate = relativePath.endsWith(".template"); - // final outputPath = - // "${dstDir.path}/${isTemplate ? relativePath.replaceFirst(".template", "") : relativePath}"; + // final outputPath = + // "${dstDir.path}/${isTemplate ? relativePath.replaceFirst(".template", "") : relativePath}"; - final String resolvedPath = await resolveTemplatePath(relativePath); + final String resolvedPath = await resolveTemplatePath(relativePath); - if (vars.containsKey("workerName") && vars["workerName"]!.isNotEmpty) { - relativePath = relativePath.replaceAll("worker", vars["workerName"]!); - } + if (vars.containsKey("workerName") && + vars["workerName"]!.isNotEmpty) { + relativePath = + relativePath.replaceAll("worker", vars["workerName"]!); + } - if (relativePath.startsWith("vite/")) { - relativePath = relativePath.replaceFirst( - "vite/", - "$targetWorkerDir/", - ); - } + if (relativePath.startsWith("vite/")) { + relativePath = relativePath.replaceFirst( + "vite/", + "$targetWorkerDir/", + ); + } - final outputPath = "${dstDir.path}/$relativePath"; - - // if (isTemplate) { - renderTemplateFile( - inputPath: resolvedPath, - outputPath: outputPath, - variables: vars, - ); - // } else { - // final outFile = File(outputPath); - // outFile.createSync(recursive: true); - // outFile.writeAsBytesSync(entity.readAsBytesSync()); - // } - - if (relativePath.endsWith(".sh")) { - Process.runSync("chmod", ["+x", outputPath]); + final outputPath = "${dstDir.path}/$relativePath"; + + // if (isTemplate) { + renderTemplateFile( + inputPath: resolvedPath, + outputPath: outputPath, + variables: vars, + ); + // } else { + // final outFile = File(outputPath); + // outFile.createSync(recursive: true); + // outFile.writeAsBytesSync(entity.readAsBytesSync()); + // } + + if (relativePath.endsWith(".sh")) { + Process.runSync("chmod", ["+x", outputPath]); + } } } + } catch (e, st) { + logger.handle(e, st); } } diff --git a/example/lib/pages/home_page.dart b/example/lib/pages/home_page.dart index e92e847..34f9814 100644 --- a/example/lib/pages/home_page.dart +++ b/example/lib/pages/home_page.dart @@ -15,7 +15,13 @@ class _HomePageState extends State { String output = "Output is empty"; - final worker = FlutterNodeWorker(path: "/workers/cipher_module.js"); + late FlutterNodeWorker worker; + + @override + void initState() { + super.initState(); + worker = FlutterNodeWorker(path: "/workers/cipher_module.js"); + } Future encryptText(String text, String psw) async { final result = await worker.compute( diff --git a/example/pubspec.lock b/example/pubspec.lock index 6d8adaa..e778ebf 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" archive: dependency: transitive description: @@ -116,7 +124,7 @@ packages: path: ".." relative: true source: path - version: "0.0.1" + version: "0.0.3" flutter_test: dependency: "direct dev" description: flutter @@ -263,6 +271,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + talker: + dependency: transitive + description: + name: talker + sha256: "95ac627afecb6267507b7a720a4713dcab84ff82179dc5794b980235aeeb5254" + url: "https://pub.dev" + source: hosted + version: "4.9.2" + talker_logger: + dependency: transitive + description: + name: talker_logger + sha256: e3c5195fa3ef2af869bf5dad26f903bf38154dc937853df3ac1991a0747a67fd + url: "https://pub.dev" + source: hosted + version: "4.9.2" term_glyph: dependency: transitive description: diff --git a/example/talker/.gitignore b/example/talker/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/example/talker/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/example/talker/index.html b/example/talker/index.html new file mode 100644 index 0000000..5bb610c --- /dev/null +++ b/example/talker/index.html @@ -0,0 +1,101 @@ + + + + + + Test Worker + + + +

Test Worker

+
+ + + + + + + +
+ + + + + +

AltStyle によって変換されたページ (->オリジナル) /

+ + \ No newline at end of file diff --git a/example/talker/package.json b/example/talker/package.json new file mode 100644 index 0000000..94ed2c3 --- /dev/null +++ b/example/talker/package.json @@ -0,0 +1,14 @@ +{ + "name": "talker", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "vite": "^7.0.4" + } +} diff --git a/example/talker/public/vite.svg b/example/talker/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/example/talker/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/example/talker/src/main.js b/example/talker/src/main.js new file mode 100644 index 0000000..20100ee --- /dev/null +++ b/example/talker/src/main.js @@ -0,0 +1,14 @@ +import Worker from "./worker?worker"; + +const worker = new Worker(); + +worker.onmessage = (e) => { + console.log("Worker result:", e.data); +}; + +worker.postMessage({ + command: "say_hello", + data: { + message: "world" + }, +}); diff --git a/example/talker/src/style.css b/example/talker/src/style.css new file mode 100644 index 0000000..8df73e3 --- /dev/null +++ b/example/talker/src/style.css @@ -0,0 +1,96 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/example/talker/src/worker.js b/example/talker/src/worker.js new file mode 100644 index 0000000..dfed997 --- /dev/null +++ b/example/talker/src/worker.js @@ -0,0 +1,53 @@ +self.postMessage(JSON.stringify({ status: "ready" })); + +self.onmessage = async function (e) { + console.log("[worker] Received message:", e.data); + + try { + const { command, data } = e.data; + + if (command === "say_hello") { + + console.log(`[worker] Command is ${command}`); + + const result = _sayHello(data.message); + + self.postMessage(JSON.stringify({ + status: "success", + command: command, + result: { + message: result, + }, + })); + + console.log("[worker] Sent success message"); + + } else if (command === "say_goodbye") { + + console.log(`[worker] Command is ${command}`); + + const result = _sayGoodbye(data.message); + + self.postMessage(JSON.stringify({ + status: "success", + command: command, + result: { + message: result, + }, + })); + + console.log("[worker] Sent success message"); + + } + } catch (error) { + self.postMessage(JSON.stringify({ status: "error", command: e.data.command, message: error.message })); + } +}; + +function _sayHello(name) { + return `Hello ${name} from Web Worker!`; +} + +function _sayGoodbye(name) { + return `Goodbye ${name} from Web Worker!`; +} \ No newline at end of file diff --git a/example/talker/vite.config.js b/example/talker/vite.config.js new file mode 100644 index 0000000..c6c142f --- /dev/null +++ b/example/talker/vite.config.js @@ -0,0 +1,52 @@ +import { defineConfig } from "vite"; +import { resolve } from 'path'; + +export default defineConfig(({ command, mode, isSsrBuild, isPreview }) => { + if (command === "serve") { + return { + // dev specific config + } + } else { + // command === "build" + return { + define: { + global: "globalThis" // важно для node-forge + }, + resolve: { + alias: { + buffer: "buffer", // чтобы node-forge мог использовать Buffer + }, + }, + optimizeDeps: { + include: [], + esbuildOptions: { + // Node.js global to browser globalThis + define: { + global: "globalThis" + // global: {}, + }, + // Enable esbuild polyfill plugins + // plugins: [ + // NodeGlobalsPolyfillPlugin({ + // buffer: true + // }) + // ] + } + }, + build: { + outDir: process.env.OUTDIR || "dist", + rollupOptions: { + input: resolve(__dirname, process.env.ENTRY || 'src/cipher.js'), + output: { + entryFileNames: () => process.env.FILENAME || 'output.js', // имя итогового файла + format: "es" // важно: чтобы работал `type: "module"` + } + }, + emptyOutDir: false, // <- не удаляем dist между сборками + target: "esnext", // чтобы не было ошибок с modern JS + minify: false, // отключи на время отладки + sourcemap: true, // удобно для отладки + } + } + } +}) \ No newline at end of file diff --git a/example/web/firebase_config.js b/example/web/firebase_config.js new file mode 100644 index 0000000..7559f30 --- /dev/null +++ b/example/web/firebase_config.js @@ -0,0 +1,17 @@ +// Import the functions you need from the SDKs you need +import { initializeApp } from "firebase/app"; +// TODO: Add SDKs for Firebase products that you want to use +// https://firebase.google.com/docs/web/setup#available-libraries + +// Your web app's Firebase configuration +const firebaseConfig = { + apiKey: "AIzaSyCYPtSXYY6k4M7uYfNY3SHERZ7xoVh58DA", + authDomain: "flutter-node-worker-demo.firebaseapp.com", + projectId: "flutter-node-worker-demo", + storageBucket: "flutter-node-worker-demo.firebasestorage.app", + messagingSenderId: "580678901315", + appId: "1:580678901315:web:368dce341061e712047ef6" +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); \ No newline at end of file diff --git a/lib/flutter_node_worker.dart b/lib/flutter_node_worker.dart index 187177a..efd34dd 100644 --- a/lib/flutter_node_worker.dart +++ b/lib/flutter_node_worker.dart @@ -1,11 +1,11 @@ -// final worker = Worker("src/worker.js", WorkerOptions(type: "module")); -// worker.postMessage({"command": "encrypt", "data": {}}); - +import 'dart:js_interop'; import 'dart:async'; import 'dart:convert'; -import 'dart:js' as js; import 'package:flutter/foundation.dart'; +import 'package:flutter_node_worker/message_event.dart'; +import 'package:flutter_node_worker/worker.dart'; +import 'package:flutter_node_worker/worker_options.dart'; /// A utility class for interacting with JavaScript Web Workers from Dart/Flutter, /// using a JavaScript module as the worker entry point. @@ -17,13 +17,15 @@ class FlutterNodeWorker { final String path; /// The internal reference to the JavaScript `Worker` object. - js.JsObject? worker; + Worker? worker; /// Creates an instance of [FlutterNodeWebworker] and spawns the worker. /// /// The [path] must point to a valid JavaScript module that can be run /// in a `Worker` environment. - FlutterNodeWorker({required this.path}) { + FlutterNodeWorker({ + required this.path, + }) { _spawn(path); } @@ -31,17 +33,15 @@ class FlutterNodeWorker { /// /// If a worker is already running, this method does nothing. void _spawn(String path) { - // terminate(); if (worker != null) return; - final workerOptions = js.JsObject.jsify({"type": "module"}); - final workerConstructor = js.context["Worker"]; - worker = js.JsObject(workerConstructor, [path, workerOptions]); + final workerOptions = WorkerOptions(type: "module"); + worker = Worker(path, workerOptions); } /// Terminates the current worker if one is active and resets the internal reference. void terminate() { - worker?.callMethod("terminate"); + worker?.terminate(); worker = null; } @@ -60,19 +60,21 @@ class FlutterNodeWorker { required String command, required Map data, bool computeOnce = false, + Duration timeoutDuration = const Duration(seconds: 10), }) async { - final completer = Completer
    ?>(); + try { + late final JSFunction jsHandler; + final completer = Completer
      ?>(); - if (worker == null) { - _spawn(path); - } + if (worker == null) { + _spawn(path); + } - void handler(jsEvent) { - final event = js.JsObject.fromBrowserObject(jsEvent); - final jsData = event["data"]; + jsHandler = ((MessageEvent jsEvent) { + final JSAny? jsData = jsEvent.data; - if (jsData is String) { - final Map parsed = jsonDecode(jsData); + final Map parsed = + jsonDecode(jsData.dartify() as String); if (kDebugMode) { print("Parsed from worker: $parsed"); @@ -87,21 +89,31 @@ class FlutterNodeWorker { terminate(); } - worker?.callMethod("removeEventListener", ["message", handler]); + worker?.removeEventListener("message", jsHandler); } - } else { - throw Exception( - "Worker has returned unexpected data type ${jsData.runtimeType}. Expected String type", - ); - } + }).toJS; + + worker?.addEventListener("message", jsHandler); + + worker?.postMessage( + ({"command": command, "data": data}).jsify(), + ); + + return completer.future.timeout( + timeoutDuration, + onTimeout: () { + worker?.removeEventListener("message", jsHandler); + if (computeOnce) terminate(); + throw TimeoutException( + "Worker timeout: no response in ${timeoutDuration.inSeconds} seconds.", + timeoutDuration, + ); + }, + ); + } catch (e) { + throw Exception( + "Worker has returned error ${e.toString()}.", + ); } - - worker?.callMethod("addEventListener", ["message", handler]); - - worker?.callMethod("postMessage", [ - js.JsObject.jsify({"command": command, "data": data}), - ]); - - return completer.future; } } diff --git a/lib/message_event.dart b/lib/message_event.dart new file mode 100644 index 0000000..03dbed2 --- /dev/null +++ b/lib/message_event.dart @@ -0,0 +1,9 @@ +import 'dart:js_interop'; + +@JS() +@staticInterop +class MessageEvent {} + +extension MessageEventExt on MessageEvent { + external JSAny? get data; +} diff --git a/lib/worker.dart b/lib/worker.dart index 2531a96..25e1830 100644 --- a/lib/worker.dart +++ b/lib/worker.dart @@ -1,15 +1,16 @@ -// @JS() -// library worker_api; - import 'dart:js_interop'; import 'package:flutter_node_worker/worker_options.dart'; -@JS('Worker') +@JS("Worker") +@staticInterop class Worker { external factory Worker(String path, WorkerOptions options); +} - external void postMessage(dynamic data); - external void addEventListener(String type, Function listener); +extension WorkerExtension on Worker { + external void postMessage(JSAny? data); + external void addEventListener(String type, JSFunction listener); + external void removeEventListener(String type, JSFunction listener); external void terminate(); } diff --git a/lib/worker_options.dart b/lib/worker_options.dart index ff6c611..6ee0a4a 100644 --- a/lib/worker_options.dart +++ b/lib/worker_options.dart @@ -2,7 +2,8 @@ import 'dart:js_interop'; @JS() @anonymous +@staticInterop class WorkerOptions { - external String get type; + // external String get type; external factory WorkerOptions({String type}); } diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 50b2cbf..965aa98 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -1,5 +1,5 @@ { - "name": "flutter_node_webworker", + "name": "flutter_node_worker", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package-lock.json b/package-lock.json index ff861f0..fd356f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "flutter_node_webworker", + "name": "flutter_node_worker", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/pubspec.yaml b/pubspec.yaml index c3019bd..6c98962 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_node_worker description: A package for using Web Workers written in Node.js with Dart in Flutter web. -version: 0.0.1 +version: 0.0.3 homepage: https://github.com/printHelloworldd/flutter-node-worker issue_tracker: https://github.com/printHelloworldd/flutter-node-worker/issues documentation: https://github.com/printHelloworldd/flutter-node-worker/blob/main/README.md @@ -9,7 +9,7 @@ platforms: web: environment: - sdk: ^3.7.2 + sdk: ">=3.0.0 <4.0.0" flutter: ">=1.17.0" dependencies: @@ -20,6 +20,7 @@ dependencies: args: ^2.7.0 mustache_template: ^2.0.0 path: ^1.9.1 + talker: ^4.9.2 dev_dependencies: flutter_test: @@ -32,7 +33,7 @@ executables: flutter: assets: - - templates/ + - lib/templates/ topics: - cli