mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
Add compute api to js engine.
This commit is contained in:
@@ -4,6 +4,18 @@ Venera JavaScript Library
|
|||||||
This library provides a set of APIs for interacting with the Venera app.
|
This library provides a set of APIs for interacting with the Venera app.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function sendMessage
|
||||||
|
* @global
|
||||||
|
* @param {Object} message
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a timeout to execute a callback function after a specified delay.
|
||||||
|
* @param callback {Function}
|
||||||
|
* @param delay {number} - delay in milliseconds
|
||||||
|
*/
|
||||||
function setTimeout(callback, delay) {
|
function setTimeout(callback, delay) {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
method: 'delay',
|
method: 'delay',
|
||||||
@@ -1412,3 +1424,18 @@ function getClipboard() {
|
|||||||
method: 'getClipboard'
|
method: 'getClipboard'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a function with arguments. The function will be executed in the engine pool which is not in the main thread.
|
||||||
|
* @param func {string} - A js code string which can be evaluated to a function. The function will receive the args as its only argument.
|
||||||
|
* @param args {any[] | null | undefined} - The arguments to pass to the function.
|
||||||
|
* @returns {Promise<any>} - The result of the function.
|
||||||
|
* @since 1.5.0
|
||||||
|
*/
|
||||||
|
function compute(func, args) {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'compute',
|
||||||
|
function: func,
|
||||||
|
args: args
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@@ -30,6 +30,10 @@ class _App {
|
|||||||
|
|
||||||
bool get isMobile => Platform.isAndroid || Platform.isIOS;
|
bool get isMobile => Platform.isAndroid || Platform.isIOS;
|
||||||
|
|
||||||
|
// Whether the app has been initialized.
|
||||||
|
// If current Isolate is main Isolate, this value is always true.
|
||||||
|
bool isInitialized = false;
|
||||||
|
|
||||||
Locale get locale {
|
Locale get locale {
|
||||||
Locale deviceLocale = PlatformDispatcher.instance.locale;
|
Locale deviceLocale = PlatformDispatcher.instance.locale;
|
||||||
if (deviceLocale.languageCode == "zh" &&
|
if (deviceLocale.languageCode == "zh" &&
|
||||||
@@ -81,6 +85,7 @@ class _App {
|
|||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
externalStoragePath = (await getExternalStorageDirectory())!.path;
|
externalStoragePath = (await getExternalStorageDirectory())!.path;
|
||||||
}
|
}
|
||||||
|
isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initComponents() async {
|
Future<void> initComponents() async {
|
||||||
|
@@ -89,8 +89,7 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
var className = line1.split("class")[1].split("extends ComicSource").first;
|
var className = line1.split("class")[1].split("extends ComicSource").first;
|
||||||
className = className.trim();
|
className = className.trim();
|
||||||
JsEngine().runCode("""
|
JsEngine().runCode("""(() => { $js
|
||||||
(() => { $js
|
|
||||||
this['temp'] = new $className()
|
this['temp'] = new $className()
|
||||||
}).call()
|
}).call()
|
||||||
""", className);
|
""", className);
|
||||||
|
@@ -24,6 +24,7 @@ import 'package:pointycastle/block/modes/ofb.dart';
|
|||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:venera/components/js_ui.dart';
|
import 'package:venera/components/js_ui.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/js_pool.dart';
|
||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/app_dio.dart';
|
||||||
import 'package:venera/network/cookie_jar.dart';
|
import 'package:venera/network/cookie_jar.dart';
|
||||||
import 'package:venera/network/proxy.dart';
|
import 'package:venera/network/proxy.dart';
|
||||||
@@ -68,6 +69,12 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
responseType: ResponseType.plain, validateStatus: (status) => true));
|
responseType: ResponseType.plain, validateStatus: (status) => true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Uint8List? _jsInitCache;
|
||||||
|
|
||||||
|
static void cacheJsInit(Uint8List jsInit) {
|
||||||
|
_jsInitCache = jsInit;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@protected
|
@protected
|
||||||
Future<void> doInit() async {
|
Future<void> doInit() async {
|
||||||
@@ -75,9 +82,11 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
if (App.isInitialized) {
|
||||||
|
_cookieJar ??= await SingleInstanceCookieJar.createInstance();
|
||||||
|
}
|
||||||
_dio ??= AppDio(BaseOptions(
|
_dio ??= AppDio(BaseOptions(
|
||||||
responseType: ResponseType.plain, validateStatus: (status) => true));
|
responseType: ResponseType.plain, validateStatus: (status) => true));
|
||||||
_cookieJar ??= await SingleInstanceCookieJar.createInstance();
|
|
||||||
_closed = false;
|
_closed = false;
|
||||||
_engine = FlutterQjs();
|
_engine = FlutterQjs();
|
||||||
_engine!.dispatch();
|
_engine!.dispatch();
|
||||||
@@ -86,9 +95,15 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
|
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
|
||||||
setGlobalFunc(["appVersion", App.version]);
|
setGlobalFunc(["appVersion", App.version]);
|
||||||
setGlobalFunc.free();
|
setGlobalFunc.free();
|
||||||
var jsInit = await rootBundle.load("assets/init.js");
|
Uint8List jsInit;
|
||||||
|
if (_jsInitCache != null) {
|
||||||
|
jsInit = _jsInitCache!;
|
||||||
|
} else {
|
||||||
|
var buffer = await rootBundle.load("assets/init.js");
|
||||||
|
jsInit = buffer.buffer.asUint8List();
|
||||||
|
}
|
||||||
_engine!
|
_engine!
|
||||||
.evaluate(utf8.decode(jsInit.buffer.asUint8List()), name: "<init>");
|
.evaluate(utf8.decode(jsInit), name: "<init>");
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error('JS Engine', 'JS Engine Init Error:\n$e\n$s');
|
Log.error('JS Engine', 'JS Engine Init Error:\n$e\n$s');
|
||||||
}
|
}
|
||||||
@@ -97,6 +112,7 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
Object? _messageReceiver(dynamic message) {
|
Object? _messageReceiver(dynamic message) {
|
||||||
try {
|
try {
|
||||||
if (message is Map<dynamic, dynamic>) {
|
if (message is Map<dynamic, dynamic>) {
|
||||||
|
if (message["method"] == null) return null;
|
||||||
String method = message["method"] as String;
|
String method = message["method"] as String;
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case "log":
|
case "log":
|
||||||
@@ -172,6 +188,20 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
var res = await Clipboard.getData(Clipboard.kTextPlain);
|
var res = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
return res?.text;
|
return res?.text;
|
||||||
});
|
});
|
||||||
|
case "compute":
|
||||||
|
final func = message["function"];
|
||||||
|
final args = message["args"];
|
||||||
|
if (func is JSInvokable) {
|
||||||
|
func.free();
|
||||||
|
throw "Function must be a string";
|
||||||
|
}
|
||||||
|
if (func is! String) {
|
||||||
|
throw "Function must be a string";
|
||||||
|
}
|
||||||
|
if (args != null && args is! List) {
|
||||||
|
throw "Args must be a list";
|
||||||
|
}
|
||||||
|
return JSPool().execute(func, args ?? []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
163
lib/foundation/js_pool.dart
Normal file
163
lib/foundation/js_pool.dart
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
|
import 'package:venera/foundation/js_engine.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
|
|
||||||
|
class JSPool {
|
||||||
|
static final int _maxInstances = 4;
|
||||||
|
final List<IsolateJsEngine> _instances = [];
|
||||||
|
bool _isInitializing = false;
|
||||||
|
|
||||||
|
static final JSPool _singleton = JSPool._internal();
|
||||||
|
factory JSPool() {
|
||||||
|
return _singleton;
|
||||||
|
}
|
||||||
|
JSPool._internal();
|
||||||
|
|
||||||
|
Future<void> init() async {
|
||||||
|
if (_isInitializing) return;
|
||||||
|
_isInitializing = true;
|
||||||
|
var jsInitBuffer = await rootBundle.load("assets/init.js");
|
||||||
|
var jsInit = jsInitBuffer.buffer.asUint8List();
|
||||||
|
for (int i = 0; i < _maxInstances; i++) {
|
||||||
|
_instances.add(IsolateJsEngine(jsInit));
|
||||||
|
}
|
||||||
|
_isInitializing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> execute(String jsFunction, List<dynamic> args) async {
|
||||||
|
await init();
|
||||||
|
var selectedInstance = _instances[0];
|
||||||
|
for (var instance in _instances) {
|
||||||
|
if (instance.pendingTasks < selectedInstance.pendingTasks) {
|
||||||
|
selectedInstance = instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedInstance.execute(jsFunction, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IsolateJsEngineInitParam {
|
||||||
|
final SendPort sendPort;
|
||||||
|
|
||||||
|
final Uint8List jsInit;
|
||||||
|
|
||||||
|
_IsolateJsEngineInitParam(this.sendPort, this.jsInit);
|
||||||
|
}
|
||||||
|
|
||||||
|
class IsolateJsEngine {
|
||||||
|
Isolate? _isolate;
|
||||||
|
|
||||||
|
SendPort? _sendPort;
|
||||||
|
ReceivePort? _receivePort;
|
||||||
|
|
||||||
|
int _counter = 0;
|
||||||
|
final Map<int, Completer<dynamic>> _tasks = {};
|
||||||
|
|
||||||
|
bool _isClosed = false;
|
||||||
|
|
||||||
|
int get pendingTasks => _tasks.length;
|
||||||
|
|
||||||
|
IsolateJsEngine(Uint8List jsInit) {
|
||||||
|
_receivePort = ReceivePort();
|
||||||
|
_receivePort!.listen(_onMessage);
|
||||||
|
Isolate.spawn(_run, _IsolateJsEngineInitParam(_receivePort!.sendPort, jsInit));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMessage(dynamic message) {
|
||||||
|
if (message is SendPort) {
|
||||||
|
_sendPort = message;
|
||||||
|
} else if (message is TaskResult) {
|
||||||
|
final completer = _tasks.remove(message.id);
|
||||||
|
if (completer != null) {
|
||||||
|
if (message.error != null) {
|
||||||
|
completer.completeError(message.error!);
|
||||||
|
} else {
|
||||||
|
completer.complete(message.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (message is Exception) {
|
||||||
|
Log.error("IsolateJsEngine", message.toString());
|
||||||
|
for (var completer in _tasks.values) {
|
||||||
|
completer.completeError(message);
|
||||||
|
}
|
||||||
|
_tasks.clear();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _run(_IsolateJsEngineInitParam params) async {
|
||||||
|
var sendPort = params.sendPort;
|
||||||
|
final port = ReceivePort();
|
||||||
|
sendPort.send(port.sendPort);
|
||||||
|
final engine = JsEngine();
|
||||||
|
try {
|
||||||
|
JsEngine.cacheJsInit(params.jsInit);
|
||||||
|
await engine.init();
|
||||||
|
}
|
||||||
|
catch(e, s) {
|
||||||
|
sendPort.send(Exception("Failed to initialize JS engine: $e\n$s"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await for (final message in port) {
|
||||||
|
if (message is Task) {
|
||||||
|
try {
|
||||||
|
final jsFunc = engine.runCode(message.jsFunction);
|
||||||
|
if (jsFunc is! JSInvokable) {
|
||||||
|
throw Exception("The provided code does not evaluate to a function.");
|
||||||
|
}
|
||||||
|
final result = jsFunc.invoke(message.args);
|
||||||
|
jsFunc.free();
|
||||||
|
sendPort.send(TaskResult(message.id, result, null));
|
||||||
|
} catch (e) {
|
||||||
|
sendPort.send(TaskResult(message.id, null, e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> execute(String jsFunction, List<dynamic> args) async {
|
||||||
|
if (_isClosed) {
|
||||||
|
throw Exception("IsolateJsEngine is closed.");
|
||||||
|
}
|
||||||
|
while (_sendPort == null) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
}
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
final taskId = _counter++;
|
||||||
|
_tasks[taskId] = completer;
|
||||||
|
final task = Task(taskId, jsFunction, args);
|
||||||
|
_sendPort?.send(task);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() async {
|
||||||
|
if (!_isClosed) {
|
||||||
|
_isClosed = true;
|
||||||
|
while (_tasks.isNotEmpty) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
_receivePort?.close();
|
||||||
|
_isolate?.kill(priority: Isolate.immediate);
|
||||||
|
_isolate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Task {
|
||||||
|
final int id;
|
||||||
|
final String jsFunction;
|
||||||
|
final List<dynamic> args;
|
||||||
|
|
||||||
|
const Task(this.id, this.jsFunction, this.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TaskResult {
|
||||||
|
final int id;
|
||||||
|
final Object? result;
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
const TaskResult(this.id, this.result, this.error);
|
||||||
|
}
|
@@ -42,7 +42,7 @@ class Log {
|
|||||||
|
|
||||||
static void addLog(LogLevel level, String title, String content) {
|
static void addLog(LogLevel level, String title, String content) {
|
||||||
if (isMuted) return;
|
if (isMuted) return;
|
||||||
if (_file == null) {
|
if (_file == null && App.isInitialized) {
|
||||||
Directory dir;
|
Directory dir;
|
||||||
if (App.isAndroid) {
|
if (App.isAndroid) {
|
||||||
dir = Directory(App.externalStoragePath!);
|
dir = Directory(App.externalStoragePath!);
|
||||||
|
@@ -112,10 +112,12 @@ class AppDio with DioMixin {
|
|||||||
AppDio([BaseOptions? options]) {
|
AppDio([BaseOptions? options]) {
|
||||||
this.options = options ?? BaseOptions();
|
this.options = options ?? BaseOptions();
|
||||||
httpClientAdapter = RHttpAdapter();
|
httpClientAdapter = RHttpAdapter();
|
||||||
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
if (App.isInitialized) {
|
||||||
interceptors.add(NetworkCacheManager());
|
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
||||||
interceptors.add(CloudflareInterceptor());
|
interceptors.add(NetworkCacheManager());
|
||||||
interceptors.add(MyLogInterceptor());
|
interceptors.add(CloudflareInterceptor());
|
||||||
|
interceptors.add(MyLogInterceptor());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static final Map<String, bool> _requests = {};
|
static final Map<String, bool> _requests = {};
|
||||||
|
@@ -202,9 +202,13 @@ class SingleInstanceCookieJar extends CookieJarSql {
|
|||||||
|
|
||||||
static SingleInstanceCookieJar? instance;
|
static SingleInstanceCookieJar? instance;
|
||||||
|
|
||||||
static Future<void> createInstance() async {
|
static Future<SingleInstanceCookieJar> createInstance() async {
|
||||||
|
if (instance != null) {
|
||||||
|
return instance!;
|
||||||
|
}
|
||||||
var dataPath = (await getApplicationSupportDirectory()).path;
|
var dataPath = (await getApplicationSupportDirectory()).path;
|
||||||
instance = SingleInstanceCookieJar("$dataPath/cookie.db");
|
instance = SingleInstanceCookieJar("$dataPath/cookie.db");
|
||||||
|
return instance!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -62,7 +62,7 @@ class DebugPageState extends State<DebugPage> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
try {
|
try {
|
||||||
var res = JsEngine().runCode(controller.text);
|
var res = JsEngine().runCode(controller.text, "<debug>");
|
||||||
setState(() {
|
setState(() {
|
||||||
result = res.toString();
|
result = res.toString();
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user