chore: introduce native implementations

- adds Client.nativeImplementations
 - deprecates Client.compute

Allows to properly implement accelerated native operations in web

Signed-off-by: Lanna Michalke <l.michalke@famedly.com>
This commit is contained in:
Lanna Michalke 2022-07-28 10:10:29 +02:00
parent 89c6b9a8c2
commit 05ff61ac86
14 changed files with 704 additions and 73 deletions

View File

@ -34,6 +34,7 @@ const megolmKey = EventTypes.MegolmBackup;
class KeyManager {
final Encryption encryption;
Client get client => encryption.client;
final outgoingShareRequests = <String, KeyManagerKeyShareRequest>{};
final incomingShareRequests = <String, KeyManagerKeyShareRequest>{};
@ -568,6 +569,7 @@ class KeyManager {
GetRoomKeysVersionCurrentResponse? _roomKeysVersionCache;
DateTime? _roomKeysVersionCacheDate;
Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo(
[bool useCache = true]) async {
if (_roomKeysVersionCache != null &&
@ -715,6 +717,7 @@ class KeyManager {
bool _isUploadingKeys = false;
bool _haveKeysToUpload = true;
Future<void> backgroundTasks() async {
final database = client.database;
final userID = client.userID;
@ -745,7 +748,7 @@ class KeyManager {
info.authData['public_key'] != backupPubKey) {
return;
}
final args = _GenerateUploadKeysArgs(
final args = GenerateUploadKeysArgs(
pubkey: backupPubKey,
dbSessions: <_DbInboundGroupSessionBundle>[],
userId: userID,
@ -768,8 +771,7 @@ class KeyManager {
}
}
final roomKeys =
await client.runInBackground<RoomKeys, _GenerateUploadKeysArgs>(
_generateUploadKeys, args);
await client.nativeImplementations.generateUploadKeys(args);
Logs().i('[Key Manager] Uploading ${dbSessions.length} room keys...');
// upload the payload...
await client.putRoomKeys(info.version, roomKeys);
@ -988,6 +990,7 @@ class KeyManagerKeyShareRequest {
class RoomKeyRequest extends ToDeviceEvent {
KeyManager keyManager;
KeyManagerKeyShareRequest request;
RoomKeyRequest.fromToDeviceEvent(
ToDeviceEvent toDeviceEvent, this.keyManager, this.request)
: super(
@ -1035,7 +1038,9 @@ class RoomKeyRequest extends ToDeviceEvent {
}
}
RoomKeys _generateUploadKeys(_GenerateUploadKeysArgs args) {
/// you would likely want to use [NativeImplementations] and
/// [Client.nativeImplementations] instead
RoomKeys generateUploadKeysImplementation(GenerateUploadKeysArgs args) {
final enc = olm.PkEncryption();
try {
enc.set_recipient_key(args.pubkey);
@ -1087,14 +1092,40 @@ class _DbInboundGroupSessionBundle {
_DbInboundGroupSessionBundle(
{required this.dbSession, required this.verified});
factory _DbInboundGroupSessionBundle.fromJson(Map<dynamic, dynamic> json) =>
_DbInboundGroupSessionBundle(
dbSession:
StoredInboundGroupSession.fromJson(Map.from(json['dbSession'])),
verified: json['verified'],
);
Map<String, Object> toJson() => {
'dbSession': dbSession.toJson(),
'verified': verified,
};
StoredInboundGroupSession dbSession;
bool verified;
}
class _GenerateUploadKeysArgs {
_GenerateUploadKeysArgs(
class GenerateUploadKeysArgs {
GenerateUploadKeysArgs(
{required this.pubkey, required this.dbSessions, required this.userId});
factory GenerateUploadKeysArgs.fromJson(Map<dynamic, dynamic> json) =>
GenerateUploadKeysArgs(
pubkey: json['pubkey'],
dbSessions: (json['dbSessions'] as Iterable)
.map((e) => _DbInboundGroupSessionBundle.fromJson(e))
.toList(),
userId: json['userId'],
);
Map<String, Object> toJson() => {
'pubkey': pubkey,
'dbSessions': dbSessions.map((e) => e.toJson()).toList(),
'userId': userId,
};
String pubkey;
List<_DbInboundGroupSessionBundle> dbSessions;
String userId;

View File

@ -215,15 +215,14 @@ class SSSS {
algorithm: AlgorithmTypes.pbkdf2,
bits: ssssKeyLength * 8,
);
privateKey = await client
.runInBackground(
_keyFromPassphrase,
_KeyFromPassphraseArgs(
passphrase: passphrase,
info: content.passphrase!,
),
)
.timeout(Duration(seconds: 10));
privateKey = await Future.value(
client.nativeImplementations.keyFromPassphrase(
KeyFromPassphraseArgs(
passphrase: passphrase,
info: content.passphrase!,
),
),
).timeout(Duration(seconds: 10));
} else {
// we need to just generate a new key from scratch
privateKey = Uint8List.fromList(uc.secureRandomBytes(ssssKeyLength));
@ -657,15 +656,14 @@ class OpenSSSS {
throw Exception(
'Tried to unlock with passphrase while key does not have a passphrase');
}
privateKey = await ssss.client
.runInBackground(
_keyFromPassphrase,
_KeyFromPassphraseArgs(
passphrase: passphrase,
info: keyData.passphrase!,
),
)
.timeout(Duration(seconds: 10));
privateKey = await Future.value(
ssss.client.nativeImplementations.keyFromPassphrase(
KeyFromPassphraseArgs(
passphrase: passphrase,
info: keyData.passphrase!,
),
),
).timeout(Duration(seconds: 10));
} else if (recoveryKey != null) {
privateKey = SSSS.decodeRecoveryKey(recoveryKey);
} else {
@ -743,13 +741,15 @@ class OpenSSSS {
}
}
class _KeyFromPassphraseArgs {
class KeyFromPassphraseArgs {
final String passphrase;
final PassphraseInfo info;
_KeyFromPassphraseArgs({required this.passphrase, required this.info});
KeyFromPassphraseArgs({required this.passphrase, required this.info});
}
Future<Uint8List> _keyFromPassphrase(_KeyFromPassphraseArgs args) async {
/// you would likely want to use [NativeImplementations] and
/// [Client.nativeImplementations] instead
Future<Uint8List> generateKeyFromPassphrase(KeyFromPassphraseArgs args) async {
return await SSSS.keyFromPassphrase(args.passphrase, args.info);
}

View File

@ -47,6 +47,7 @@ export 'src/utils/matrix_file.dart';
export 'src/utils/matrix_id_string_extension.dart';
export 'src/utils/matrix_default_localizations.dart';
export 'src/utils/matrix_localizations.dart';
export 'src/utils/native_implementations.dart';
export 'src/utils/push_notification.dart';
export 'src/utils/receipt.dart';
export 'src/utils/sync_update_extension.dart';
@ -56,3 +57,9 @@ export 'src/utils/uri_extension.dart';
export 'msc_extensions/extension_recent_emoji/recent_emoji.dart';
export 'msc_extensions/msc_1236_widgets/msc_1236_widgets.dart';
export 'src/utils/web_worker/web_worker_stub.dart'
if (dart.library.html) 'src/utils/web_worker/web_worker.dart';
export 'src/utils/web_worker/native_implementations_web_worker_stub.dart'
if (dart.library.html) 'src/utils/web_worker/native_implementations_web_worker.dart';

View File

@ -33,6 +33,7 @@ import 'package:matrix/src/utils/sync_update_item_count.dart';
import '../encryption.dart';
import '../matrix.dart';
import 'models/timeline_chunk.dart';
import 'utils/compute_callback.dart';
import 'utils/multilock.dart';
import 'utils/run_benchmarked.dart';
@ -90,11 +91,13 @@ class Client extends MatrixApi {
final Map<String, FutureOr<String?> Function(CommandArgs)> commands = {};
final Filter syncFilter;
final NativeImplementations nativeImplementations;
String? syncFilterId;
final Future<R> Function<Q, R>(FutureOr<R> Function(Q), Q,
{String debugLabel})? compute;
final ComputeCallback? compute;
@Deprecated('Use [nativeImplementations] instead')
Future<T> runInBackground<T, U>(
FutureOr<T> Function(U arg) function, U arg) async {
final compute = this.compute;
@ -142,8 +145,8 @@ class Client extends MatrixApi {
/// If your client supports more login types like login with token or SSO, then add this to
/// [supportedLoginTypes]. Set a custom [syncFilter] if you like. By default the app
/// will use lazy_load_members.
/// Set [compute] to the Flutter compute method to enable the SDK to run some
/// code in background.
/// Set [nativeImplementations] to [NativeImplementationsIsolate] in order to
/// enable the SDK to compute some code in background.
/// Set [timelineEventTimeout] to the preferred time the Client should retry
/// sending events on connection problems or to `Duration.zero` to disable it.
/// Set [customImageResizer] to your own implementation for a more advanced
@ -165,7 +168,8 @@ class Client extends MatrixApi {
Set<String>? supportedLoginTypes,
this.mxidLocalPartFallback = true,
this.formatLocalpart = true,
this.compute,
@Deprecated('Use [nativeImplementations] instead') this.compute,
NativeImplementations nativeImplementations = NativeImplementations.dummy,
Level? logLevel,
Filter? syncFilter,
this.sendTimelineEventTimeout = const Duration(minutes: 1),
@ -182,6 +186,9 @@ class Client extends MatrixApi {
supportedLoginTypes =
supportedLoginTypes ?? {AuthenticationTypes.password},
verificationMethods = verificationMethods ?? <KeyVerificationMethod>{},
nativeImplementations = compute != null
? NativeImplementationsIsolate(compute)
: nativeImplementations,
super(
httpClient:
VariableTimeoutHttpClient(httpClient ?? http.Client())) {
@ -227,7 +234,10 @@ class Client extends MatrixApi {
String? _deviceName;
// for group calls
// A unique identifier used for resolving duplicate group call sessions from a given device. When the session_id field changes from an incoming m.call.member event, any existing calls from this device in this call should be terminated. The id is generated once per client load.
// A unique identifier used for resolving duplicate group call
// sessions from a given device. When the session_id field changes from
// an incoming m.call.member event, any existing calls from this device in
// this call should be terminated. The id is generated once per client load.
String? get groupCallSessionId => _groupCallSessionId;
String? _groupCallSessionId;
@ -2906,5 +2916,6 @@ class HomeserverSummary {
class ArchivedRoom {
final Room room;
final Timeline timeline;
ArchivedRoom({required this.room, required this.timeline});
}

View File

@ -617,8 +617,8 @@ class Event extends MatrixEvent {
k: fileMap['key']['k'],
sha256: fileMap['hashes']['sha256'],
);
uint8list = await room.client.runInBackground<Uint8List?, EncryptedFile>(
decryptFile, encryptedFile);
uint8list =
await room.client.nativeImplementations.decryptFile(encryptedFile);
if (uint8list == null) {
throw ('Unable to decrypt file');
}

View File

@ -721,7 +721,7 @@ class Room {
FileSendingStatus.generatingThumbnail.name;
await _handleFakeSync(syncUpdate);
thumbnail ??= await file.generateThumbnail(
compute: client.runInBackground,
nativeImplementations: client.nativeImplementations,
customImageResizer: client.customImageResizer,
);
if (shrinkImageMaxDimension != null) {
@ -730,7 +730,7 @@ class Room {
name: file.name,
maxDimension: shrinkImageMaxDimension,
customImageResizer: client.customImageResizer,
compute: client.runInBackground,
nativeImplementations: client.nativeImplementations,
);
}
@ -942,7 +942,7 @@ class Room {
? inReplyTo.formattedText
: htmlEscape.convert(inReplyTo.body).replaceAll('\n', '<br>'))
.replaceAll(
RegExp(r'<mx-reply>.*<\/mx-reply>',
RegExp(r'<mx-reply>.*</mx-reply>',
caseSensitive: false, multiLine: false, dotAll: true),
'');
final repliedHtml = content.tryGet<String>('formatted_body') ??

View File

@ -0,0 +1,15 @@
import 'dart:async';
typedef ComputeCallback = Future<R> Function<Q, R>(
FutureOr<R> Function(Q message) callback, Q message,
{String? debugLabel});
// keep types in sync with [computeCallbackFromRunInBackground]
typedef ComputeRunner = Future<T> Function<T, U>(
FutureOr<T> Function(U arg) function, U arg);
ComputeCallback computeCallbackFromRunInBackground(ComputeRunner runner) {
return <U, T>(FutureOr<T> Function(U arg) callback, U arg,
{String? debugLabel}) =>
runner.call(callback, arg);
}

View File

@ -48,7 +48,9 @@ Future<EncryptedFile> encryptFile(Uint8List input) async {
);
}
Future<Uint8List?> decryptFile(EncryptedFile input) async {
/// you would likely want to use [NativeImplementations] and
/// [Client.nativeImplementations] instead
Future<Uint8List?> decryptFileImplementation(EncryptedFile input) async {
if (base64.encode(await sha256(input.data)) !=
base64.normalize(input.sha256)) {
return null;

View File

@ -26,6 +26,7 @@ import 'package:image/image.dart';
import 'package:mime/mime.dart';
import '../../matrix.dart';
import 'compute_callback.dart';
class MatrixFile {
final Uint8List bytes;
@ -78,15 +79,18 @@ class MatrixImageFile extends MatrixFile {
super(bytes: bytes, name: name, mimeType: mimeType);
/// Creates a new image file and calculates the width, height and blurhash.
static Future<MatrixImageFile> create(
{required Uint8List bytes,
required String name,
String? mimeType,
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
compute}) async {
final metaData = compute != null
? await compute(_calcMetadata, bytes)
: _calcMetadata(bytes);
static Future<MatrixImageFile> create({
required Uint8List bytes,
required String name,
String? mimeType,
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
NativeImplementations nativeImplementations = NativeImplementations.dummy,
}) async {
if (compute != null) {
nativeImplementations =
NativeImplementationsIsolate.fromRunInBackground(compute);
}
final metaData = await nativeImplementations.calcImageMetadata(bytes);
return MatrixImageFile(
bytes: metaData?.bytes ?? bytes,
@ -101,22 +105,27 @@ class MatrixImageFile extends MatrixFile {
/// Builds a [MatrixImageFile] and shrinks it in order to reduce traffic.
/// If shrinking does not work (e.g. for unsupported MIME types), the
/// initial image is preserved without shrinking it.
static Future<MatrixImageFile> shrink(
{required Uint8List bytes,
required String name,
int maxDimension = 1600,
String? mimeType,
Future<MatrixImageFileResizedResponse?> Function(
MatrixImageFileResizeArguments)?
customImageResizer,
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
compute}) async {
static Future<MatrixImageFile> shrink({
required Uint8List bytes,
required String name,
int maxDimension = 1600,
String? mimeType,
Future<MatrixImageFileResizedResponse?> Function(
MatrixImageFileResizeArguments)?
customImageResizer,
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
NativeImplementations nativeImplementations = NativeImplementations.dummy,
}) async {
if (compute != null) {
nativeImplementations =
NativeImplementationsIsolate.fromRunInBackground(compute);
}
final image = MatrixImageFile(name: name, mimeType: mimeType, bytes: bytes);
return await image.generateThumbnail(
dimension: maxDimension,
customImageResizer: customImageResizer,
compute: compute) ??
nativeImplementations: nativeImplementations) ??
image;
}
@ -141,6 +150,7 @@ class MatrixImageFile extends MatrixFile {
@override
String get msgType => 'm.image';
@override
Map<String, dynamic> get info => ({
...super.info,
@ -151,13 +161,18 @@ class MatrixImageFile extends MatrixFile {
/// Computes a thumbnail for the image.
/// Also sets height and width on the original image if they were unset.
Future<MatrixImageFile?> generateThumbnail(
{int dimension = Client.defaultThumbnailSize,
Future<MatrixImageFileResizedResponse?> Function(
MatrixImageFileResizeArguments)?
customImageResizer,
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
compute}) async {
Future<MatrixImageFile?> generateThumbnail({
int dimension = Client.defaultThumbnailSize,
Future<MatrixImageFileResizedResponse?> Function(
MatrixImageFileResizeArguments)?
customImageResizer,
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
NativeImplementations nativeImplementations = NativeImplementations.dummy,
}) async {
if (compute != null) {
nativeImplementations =
NativeImplementationsIsolate.fromRunInBackground(compute);
}
final arguments = MatrixImageFileResizeArguments(
bytes: bytes,
maxDimension: dimension,
@ -166,19 +181,17 @@ class MatrixImageFile extends MatrixFile {
);
final resizedData = customImageResizer != null
? await customImageResizer(arguments)
: compute != null
? await compute(_resize, arguments)
: _resize(arguments);
: await nativeImplementations.shrinkImage(arguments);
if (resizedData == null) {
return null;
}
// we should take the opportinity to update the image dimmension
// we should take the opportunity to update the image dimension
setImageSizeIfNull(
width: resizedData.originalWidth, height: resizedData.originalHeight);
// the thumbnail should rather return null than the unshrinked image
// the thumbnail should rather return null than the enshrined image
if (resizedData.width > dimension || resizedData.height > dimension) {
return null;
}
@ -194,7 +207,10 @@ class MatrixImageFile extends MatrixFile {
return thumbnailFile;
}
static MatrixImageFileResizedResponse? _calcMetadata(Uint8List bytes) {
/// you would likely want to use [NativeImplementations] and
/// [Client.nativeImplementations] instead
static MatrixImageFileResizedResponse? calcMetadataImplementation(
Uint8List bytes) {
final image = decodeImage(bytes);
if (image == null) return null;
@ -210,7 +226,9 @@ class MatrixImageFile extends MatrixFile {
);
}
static MatrixImageFileResizedResponse? _resize(
/// you would likely want to use [NativeImplementations] and
/// [Client.nativeImplementations] instead
static MatrixImageFileResizedResponse? resizeImplementation(
MatrixImageFileResizeArguments arguments) {
final image = decodeImage(arguments.bytes);
@ -255,6 +273,28 @@ class MatrixImageFileResizedResponse {
this.originalWidth,
this.blurhash,
});
factory MatrixImageFileResizedResponse.fromJson(
Map<String, dynamic> json,
) =>
MatrixImageFileResizedResponse(
bytes: Uint8List.fromList(
(json['bytes'] as Iterable<dynamic>).whereType<int>().toList()),
width: json['width'],
height: json['height'],
originalHeight: json['originalHeight'],
originalWidth: json['originalWidth'],
blurhash: json['blurhash'],
);
Map<String, dynamic> toJson() => {
'bytes': bytes,
'width': width,
'height': height,
if (blurhash != null) 'blurhash': blurhash,
if (originalHeight != null) 'originalHeight': originalHeight,
if (originalWidth != null) 'originalWidth': originalWidth,
};
}
class MatrixImageFileResizeArguments {
@ -269,6 +309,21 @@ class MatrixImageFileResizeArguments {
required this.fileName,
required this.calcBlurhash,
});
factory MatrixImageFileResizeArguments.fromJson(Map<String, dynamic> json) =>
MatrixImageFileResizeArguments(
bytes: json['bytes'],
maxDimension: json['maxDimension'],
fileName: json['fileName'],
calcBlurhash: json['calcBlurhash'],
);
Map<String, Object> toJson() => {
'bytes': bytes,
'maxDimension': maxDimension,
'fileName': fileName,
'calcBlurhash': calcBlurhash,
};
}
class MatrixVideoFile extends MatrixFile {
@ -284,8 +339,10 @@ class MatrixVideoFile extends MatrixFile {
this.height,
this.duration})
: super(bytes: bytes, name: name, mimeType: mimeType);
@override
String get msgType => 'm.video';
@override
Map<String, dynamic> get info => ({
...super.info,
@ -304,8 +361,10 @@ class MatrixAudioFile extends MatrixFile {
String? mimeType,
this.duration})
: super(bytes: bytes, name: name, mimeType: mimeType);
@override
String get msgType => 'm.audio';
@override
Map<String, dynamic> get info => ({
...super.info,

View File

@ -0,0 +1,165 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:matrix/encryption.dart';
import 'package:matrix/matrix.dart';
import 'compute_callback.dart';
/// provides native implementations for demanding arithmetic operations
/// in order to prevent the UI from blocking
///
/// possible implementations might be:
/// - native code
/// - another Dart isolate
/// - a web worker
/// - a dummy implementations
///
/// Rules for extension (important for [noSuchMethod] implementations)
/// - always only accept exactly *one* positioned argument
/// - catch the corresponding case in [NativeImplementations.noSuchMethod]
/// - always write a dummy implementations
abstract class NativeImplementations {
const NativeImplementations();
/// a dummy implementation executing all calls in the same thread causing
/// the UI to likely freeze
static const dummy = NativeImplementationsDummy();
FutureOr<RoomKeys> generateUploadKeys(GenerateUploadKeysArgs args);
FutureOr<Uint8List> keyFromPassphrase(KeyFromPassphraseArgs args);
FutureOr<Uint8List?> decryptFile(EncryptedFile file);
FutureOr<MatrixImageFileResizedResponse?> shrinkImage(
MatrixImageFileResizeArguments args);
FutureOr<MatrixImageFileResizedResponse?> calcImageMetadata(Uint8List bytes);
@override
/// this implementation will catch any non-implemented method
dynamic noSuchMethod(Invocation invocation) {
final dynamic argument = invocation.positionalArguments.single;
final memberName = invocation.memberName.toString().split('"')[1];
Logs().w(
'Missing implementations of Client.nativeImplementations.$memberName. '
'You should consider implementing it. '
'Fallback from NativeImplementations.dummy used.',
);
switch (memberName) {
case 'generateUploadKeys':
return dummy.generateUploadKeys(argument);
case 'keyFromPassphrase':
return dummy.keyFromPassphrase(argument);
case 'decryptFile':
return dummy.decryptFile(argument);
case 'shrinkImage':
return dummy.shrinkImage(argument);
case 'calcImageMetadata':
return dummy.calcImageMetadata(argument);
default:
return super.noSuchMethod(invocation);
}
}
}
class NativeImplementationsDummy extends NativeImplementations {
const NativeImplementationsDummy();
@override
Future<Uint8List?> decryptFile(EncryptedFile file) {
return decryptFileImplementation(file);
}
@override
Future<RoomKeys> generateUploadKeys(GenerateUploadKeysArgs args) async {
return generateUploadKeysImplementation(args);
}
@override
Future<Uint8List> keyFromPassphrase(KeyFromPassphraseArgs args) {
return generateKeyFromPassphrase(args);
}
@override
MatrixImageFileResizedResponse? shrinkImage(
MatrixImageFileResizeArguments args) {
return MatrixImageFile.resizeImplementation(args);
}
@override
MatrixImageFileResizedResponse? calcImageMetadata(Uint8List bytes) {
return MatrixImageFile.calcMetadataImplementation(bytes);
}
}
/// a [NativeImplementations] based on Flutter's `compute` function
///
/// this implementations simply wraps the given [compute] function around
/// the implementation of [NativeImplementations.dummy]
class NativeImplementationsIsolate extends NativeImplementations {
/// pass by Flutter's compute function here
final ComputeCallback compute;
NativeImplementationsIsolate(this.compute);
/// creates a [NativeImplementationsIsolate] based on a [ComputeRunner] as
// ignore: deprecated_member_use_from_same_package
/// known from [Client.runInBackground]
factory NativeImplementationsIsolate.fromRunInBackground(
ComputeRunner runInBackground) {
return NativeImplementationsIsolate(
computeCallbackFromRunInBackground(runInBackground),
);
}
Future<T> runInBackground<T, U>(
FutureOr<T> Function(U arg) function, U arg) async {
final compute = this.compute;
return await compute(function, arg);
}
@override
Future<Uint8List?> decryptFile(EncryptedFile file) {
return runInBackground<Uint8List?, EncryptedFile>(
NativeImplementations.dummy.decryptFile,
file,
);
}
@override
Future<RoomKeys> generateUploadKeys(GenerateUploadKeysArgs args) async {
return runInBackground<RoomKeys, GenerateUploadKeysArgs>(
NativeImplementations.dummy.generateUploadKeys,
args,
);
}
@override
Future<Uint8List> keyFromPassphrase(KeyFromPassphraseArgs args) {
return runInBackground<Uint8List, KeyFromPassphraseArgs>(
NativeImplementations.dummy.keyFromPassphrase,
args,
);
}
@override
Future<MatrixImageFileResizedResponse?> shrinkImage(
MatrixImageFileResizeArguments args) {
return runInBackground<MatrixImageFileResizedResponse?,
MatrixImageFileResizeArguments>(
NativeImplementations.dummy.shrinkImage,
args,
);
}
@override
FutureOr<MatrixImageFileResizedResponse?> calcImageMetadata(Uint8List bytes) {
return runInBackground<MatrixImageFileResizedResponse?, Uint8List>(
NativeImplementations.dummy.calcImageMetadata,
bytes,
);
}
}

View File

@ -0,0 +1,157 @@
import 'dart:async';
import 'dart:collection';
import 'dart:html';
import 'dart:math';
import 'dart:typed_data';
import 'package:matrix/encryption.dart';
import 'package:matrix/matrix.dart';
class NativeImplementationsWebWorker extends NativeImplementations {
final Worker worker;
final Duration timeout;
final WebWorkerStackTraceCallback onStackTrace;
final Map<double, Completer<dynamic>> _completers = {};
final _random = Random();
/// the default handler for stackTraces in web workers
static StackTrace defaultStackTraceHandler(String obfuscatedStackTrace) {
return StackTrace.fromString(obfuscatedStackTrace);
}
NativeImplementationsWebWorker(
Uri href, {
this.timeout = const Duration(seconds: 30),
this.onStackTrace = defaultStackTraceHandler,
}) : worker = Worker(href.toString()) {
worker.onMessage.listen(_handleIncomingMessage);
}
Future<T> operation<T, U>(WebWorkerOperations name, U argument) async {
final label = _random.nextDouble();
final completer = Completer<T>();
_completers[label] = completer;
final message = WebWorkerData(label, name, argument);
worker.postMessage(message.toJson());
return completer.future.timeout(timeout);
}
void _handleIncomingMessage(MessageEvent event) {
final data = event.data;
// don't forget handling errors of our second thread...
if (data['label'] == 'stacktrace') {
final origin = event.data['origin'];
final completer = _completers[origin];
final error = event.data['error']!;
Future.value(
onStackTrace.call(event.data['stacktrace'] as String),
).then(
(stackTrace) => completer?.completeError(
WebWorkerError(error: error, stackTrace: stackTrace),
),
);
} else {
final response = WebWorkerData.fromJson(event.data);
_completers[response.label]!.complete(response.data);
}
}
@override
Future<MatrixImageFileResizedResponse?> calcImageMetadata(
Uint8List bytes) async {
try {
final result = await operation<Map<dynamic, dynamic>, Uint8List>(
WebWorkerOperations.calcImageMetadata,
bytes,
);
return MatrixImageFileResizedResponse.fromJson(Map.from(result));
} catch (e, s) {
Logs().e('Web worker computation error. Fallback to main thread', e, s);
return NativeImplementations.dummy.calcImageMetadata(bytes);
}
}
@override
Future<MatrixImageFileResizedResponse?> shrinkImage(
MatrixImageFileResizeArguments args) async {
try {
final result =
await operation<Map<dynamic, dynamic>, Map<String, dynamic>>(
WebWorkerOperations.shrinkImage,
args.toJson(),
);
return MatrixImageFileResizedResponse.fromJson(Map.from(result));
} catch (e, s) {
Logs().e('Web worker computation error. Fallback to main thread', e, s);
return NativeImplementations.dummy.shrinkImage(args);
}
}
@override
Future<RoomKeys> generateUploadKeys(GenerateUploadKeysArgs args) async {
try {
final result =
await operation<Map<dynamic, dynamic>, Map<String, dynamic>>(
WebWorkerOperations.generateUploadKeys,
args.toJson(),
);
return RoomKeys.fromJson(Map.from(result));
} catch (e, s) {
Logs().e('Web worker computation error. Fallback to main thread', e, s);
return NativeImplementations.dummy.generateUploadKeys(args);
}
}
}
class WebWorkerData {
final Object? label;
final WebWorkerOperations? name;
final Object? data;
const WebWorkerData(this.label, this.name, this.data);
factory WebWorkerData.fromJson(LinkedHashMap<dynamic, dynamic> data) =>
WebWorkerData(
data['label'],
data.containsKey('name')
? WebWorkerOperations.values[data['name']]
: null,
data['data'],
);
Map<String, Object?> toJson() => {
'label': label,
if (name != null) 'name': name!.index,
'data': data,
};
}
enum WebWorkerOperations {
shrinkImage,
calcImageMetadata,
generateUploadKeys,
}
class WebWorkerError extends Error {
/// the error thrown in the web worker. Usually a [String]
final Object? error;
/// de-serialized [StackTrace]
@override
final StackTrace stackTrace;
WebWorkerError({required this.error, required this.stackTrace});
@override
String toString() {
return '$error, $stackTrace';
}
}
/// converts a stringifyed, obfuscated [StackTrace] into a [StackTrace]
typedef WebWorkerStackTraceCallback = FutureOr<StackTrace> Function(
String obfuscatedStackTrace);

View File

@ -0,0 +1,36 @@
import 'dart:async';
import 'package:matrix/matrix.dart';
class NativeImplementationsWebWorker extends NativeImplementations {
/// the default handler for stackTraces in web workers
static StackTrace defaultStackTraceHandler(String obfuscatedStackTrace) {
return StackTrace.fromString(obfuscatedStackTrace);
}
NativeImplementationsWebWorker(
Uri href, {
Duration timeout = const Duration(seconds: 30),
WebWorkerStackTraceCallback onStackTrace = defaultStackTraceHandler,
});
}
class WebWorkerError extends Error {
/// the error thrown in the web worker. Usually a [String]
final Object? error;
/// de-serialized [StackTrace]
@override
final StackTrace stackTrace;
WebWorkerError({required this.error, required this.stackTrace});
@override
String toString() {
return '$error, $stackTrace';
}
}
/// converts a stringifyed, obfuscated [StackTrace] into a [StackTrace]
typedef WebWorkerStackTraceCallback = FutureOr<StackTrace> Function(
String obfuscatedStackTrace);

View File

@ -0,0 +1,125 @@
// ignore_for_file: avoid_print
import 'dart:async';
import 'dart:html';
import 'dart:indexed_db';
import 'dart:js';
import 'dart:typed_data';
import 'package:js/js.dart';
import 'package:js/js_util.dart';
import 'package:matrix/encryption.dart';
import 'package:matrix/matrix.dart' hide Event;
import 'native_implementations_web_worker.dart';
///
///
/// CAUTION: THIS FILE NEEDS TO BE MANUALLY COMPILED
///
/// 1. in your project, create a file `web/web_worker.dart`
/// 2. add the following contents:
/// ```dart
/// import 'package:hive/hive.dart';
///
/// Future<void> main() => startWebWorker();
/// ```
/// 3. compile the file using:
/// ```shell
/// dart compile js -o web/web_worker.dart.js -m web/web_worker.dart
/// ```
///
/// You should not check in that file into your VCS. Instead, you should compile
/// the web worker in your CI pipeline.
///
@pragma('dart2js:tryInline')
Future<void> startWebWorker() async {
print('[native implementations worker]: Starting...');
setProperty(
context['self'] as Object,
'onmessage',
allowInterop(
(MessageEvent event) async {
final data = event.data;
try {
final operation = WebWorkerData.fromJson(data);
switch (operation.name) {
case WebWorkerOperations.shrinkImage:
final result = MatrixImageFile.resizeImplementation(
MatrixImageFileResizeArguments.fromJson(
Map.from(operation.data as Map)));
sendResponse(operation.label as double, result?.toJson());
break;
case WebWorkerOperations.calcImageMetadata:
final result = MatrixImageFile.calcMetadataImplementation(
Uint8List.fromList(
(operation.data as JsArray).whereType<int>().toList()));
sendResponse(operation.label as double, result?.toJson());
break;
case WebWorkerOperations.generateUploadKeys:
final result = generateUploadKeysImplementation(
GenerateUploadKeysArgs.fromJson(
Map.from(operation.data as Map),
),
);
sendResponse(operation.label as double, result.toJson());
break;
default:
throw NullThrownError();
}
} on Event catch (e, s) {
allowInterop(_replyError)
.call((e.target as Request).error, s, data['label'] as double);
} catch (e, s) {
allowInterop(_replyError).call(e, s, data['label'] as double);
}
},
),
);
}
void sendResponse(double label, dynamic response) {
try {
self.postMessage({
'label': label,
'data': response,
});
} catch (e, s) {
print('[native implementations worker] Error responding: $e, $s');
}
}
void _replyError(Object? error, StackTrace stackTrace, double origin) {
if (error != null) {
try {
final jsError = jsify(error);
if (jsError != null) {
error = jsError;
}
} catch (e) {
error = error.toString();
}
}
try {
self.postMessage({
'label': 'stacktrace',
'origin': origin,
'error': error,
'stacktrace': stackTrace.toString(),
});
} catch (e, s) {
print('[native implementations worker] Error responding: $e, $s');
}
}
/// represents the [WorkerGlobalScope] the worker currently runs in.
@JS('self')
external WorkerGlobalScope get self;
/// adding all missing WebWorker-only properties to the [WorkerGlobalScope]
extension on WorkerGlobalScope {
void postMessage(Object data) {
callMethod(self, 'postMessage', [jsify(data)]);
}
}

View File

@ -0,0 +1,23 @@
import 'dart:async';
///
///
/// CAUTION: THIS FILE NEEDS TO BE MANUALLY COMPILED
///
/// 1. in your project, create a file `web/web_worker.dart`
/// 2. add the following contents:
/// ```dart
/// import 'package:hive/hive.dart';
///
/// Future<void> main() => startWebWorker();
/// ```
/// 3. compile the file using:
/// ```shell
/// dart compile js -o web/web_worker.dart.js -m web/web_worker.dart
/// ```
///
/// You should not check in that file into your VCS. Instead, you should compile
/// the web worker in your CI pipeline.
///
Future<void> startWebWorker() => Future.value();