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:
parent
89c6b9a8c2
commit
05ff61ac86
|
|
@ -34,6 +34,7 @@ const megolmKey = EventTypes.MegolmBackup;
|
||||||
|
|
||||||
class KeyManager {
|
class KeyManager {
|
||||||
final Encryption encryption;
|
final Encryption encryption;
|
||||||
|
|
||||||
Client get client => encryption.client;
|
Client get client => encryption.client;
|
||||||
final outgoingShareRequests = <String, KeyManagerKeyShareRequest>{};
|
final outgoingShareRequests = <String, KeyManagerKeyShareRequest>{};
|
||||||
final incomingShareRequests = <String, KeyManagerKeyShareRequest>{};
|
final incomingShareRequests = <String, KeyManagerKeyShareRequest>{};
|
||||||
|
|
@ -568,6 +569,7 @@ class KeyManager {
|
||||||
|
|
||||||
GetRoomKeysVersionCurrentResponse? _roomKeysVersionCache;
|
GetRoomKeysVersionCurrentResponse? _roomKeysVersionCache;
|
||||||
DateTime? _roomKeysVersionCacheDate;
|
DateTime? _roomKeysVersionCacheDate;
|
||||||
|
|
||||||
Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo(
|
Future<GetRoomKeysVersionCurrentResponse> getRoomKeysBackupInfo(
|
||||||
[bool useCache = true]) async {
|
[bool useCache = true]) async {
|
||||||
if (_roomKeysVersionCache != null &&
|
if (_roomKeysVersionCache != null &&
|
||||||
|
|
@ -715,6 +717,7 @@ class KeyManager {
|
||||||
|
|
||||||
bool _isUploadingKeys = false;
|
bool _isUploadingKeys = false;
|
||||||
bool _haveKeysToUpload = true;
|
bool _haveKeysToUpload = true;
|
||||||
|
|
||||||
Future<void> backgroundTasks() async {
|
Future<void> backgroundTasks() async {
|
||||||
final database = client.database;
|
final database = client.database;
|
||||||
final userID = client.userID;
|
final userID = client.userID;
|
||||||
|
|
@ -745,7 +748,7 @@ class KeyManager {
|
||||||
info.authData['public_key'] != backupPubKey) {
|
info.authData['public_key'] != backupPubKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final args = _GenerateUploadKeysArgs(
|
final args = GenerateUploadKeysArgs(
|
||||||
pubkey: backupPubKey,
|
pubkey: backupPubKey,
|
||||||
dbSessions: <_DbInboundGroupSessionBundle>[],
|
dbSessions: <_DbInboundGroupSessionBundle>[],
|
||||||
userId: userID,
|
userId: userID,
|
||||||
|
|
@ -768,8 +771,7 @@ class KeyManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final roomKeys =
|
final roomKeys =
|
||||||
await client.runInBackground<RoomKeys, _GenerateUploadKeysArgs>(
|
await client.nativeImplementations.generateUploadKeys(args);
|
||||||
_generateUploadKeys, args);
|
|
||||||
Logs().i('[Key Manager] Uploading ${dbSessions.length} room keys...');
|
Logs().i('[Key Manager] Uploading ${dbSessions.length} room keys...');
|
||||||
// upload the payload...
|
// upload the payload...
|
||||||
await client.putRoomKeys(info.version, roomKeys);
|
await client.putRoomKeys(info.version, roomKeys);
|
||||||
|
|
@ -988,6 +990,7 @@ class KeyManagerKeyShareRequest {
|
||||||
class RoomKeyRequest extends ToDeviceEvent {
|
class RoomKeyRequest extends ToDeviceEvent {
|
||||||
KeyManager keyManager;
|
KeyManager keyManager;
|
||||||
KeyManagerKeyShareRequest request;
|
KeyManagerKeyShareRequest request;
|
||||||
|
|
||||||
RoomKeyRequest.fromToDeviceEvent(
|
RoomKeyRequest.fromToDeviceEvent(
|
||||||
ToDeviceEvent toDeviceEvent, this.keyManager, this.request)
|
ToDeviceEvent toDeviceEvent, this.keyManager, this.request)
|
||||||
: super(
|
: 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();
|
final enc = olm.PkEncryption();
|
||||||
try {
|
try {
|
||||||
enc.set_recipient_key(args.pubkey);
|
enc.set_recipient_key(args.pubkey);
|
||||||
|
|
@ -1087,14 +1092,40 @@ class _DbInboundGroupSessionBundle {
|
||||||
_DbInboundGroupSessionBundle(
|
_DbInboundGroupSessionBundle(
|
||||||
{required this.dbSession, required this.verified});
|
{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;
|
StoredInboundGroupSession dbSession;
|
||||||
bool verified;
|
bool verified;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GenerateUploadKeysArgs {
|
class GenerateUploadKeysArgs {
|
||||||
_GenerateUploadKeysArgs(
|
GenerateUploadKeysArgs(
|
||||||
{required this.pubkey, required this.dbSessions, required this.userId});
|
{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;
|
String pubkey;
|
||||||
List<_DbInboundGroupSessionBundle> dbSessions;
|
List<_DbInboundGroupSessionBundle> dbSessions;
|
||||||
String userId;
|
String userId;
|
||||||
|
|
|
||||||
|
|
@ -215,15 +215,14 @@ class SSSS {
|
||||||
algorithm: AlgorithmTypes.pbkdf2,
|
algorithm: AlgorithmTypes.pbkdf2,
|
||||||
bits: ssssKeyLength * 8,
|
bits: ssssKeyLength * 8,
|
||||||
);
|
);
|
||||||
privateKey = await client
|
privateKey = await Future.value(
|
||||||
.runInBackground(
|
client.nativeImplementations.keyFromPassphrase(
|
||||||
_keyFromPassphrase,
|
KeyFromPassphraseArgs(
|
||||||
_KeyFromPassphraseArgs(
|
passphrase: passphrase,
|
||||||
passphrase: passphrase,
|
info: content.passphrase!,
|
||||||
info: content.passphrase!,
|
),
|
||||||
),
|
),
|
||||||
)
|
).timeout(Duration(seconds: 10));
|
||||||
.timeout(Duration(seconds: 10));
|
|
||||||
} else {
|
} else {
|
||||||
// we need to just generate a new key from scratch
|
// we need to just generate a new key from scratch
|
||||||
privateKey = Uint8List.fromList(uc.secureRandomBytes(ssssKeyLength));
|
privateKey = Uint8List.fromList(uc.secureRandomBytes(ssssKeyLength));
|
||||||
|
|
@ -657,15 +656,14 @@ class OpenSSSS {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Tried to unlock with passphrase while key does not have a passphrase');
|
'Tried to unlock with passphrase while key does not have a passphrase');
|
||||||
}
|
}
|
||||||
privateKey = await ssss.client
|
privateKey = await Future.value(
|
||||||
.runInBackground(
|
ssss.client.nativeImplementations.keyFromPassphrase(
|
||||||
_keyFromPassphrase,
|
KeyFromPassphraseArgs(
|
||||||
_KeyFromPassphraseArgs(
|
passphrase: passphrase,
|
||||||
passphrase: passphrase,
|
info: keyData.passphrase!,
|
||||||
info: keyData.passphrase!,
|
),
|
||||||
),
|
),
|
||||||
)
|
).timeout(Duration(seconds: 10));
|
||||||
.timeout(Duration(seconds: 10));
|
|
||||||
} else if (recoveryKey != null) {
|
} else if (recoveryKey != null) {
|
||||||
privateKey = SSSS.decodeRecoveryKey(recoveryKey);
|
privateKey = SSSS.decodeRecoveryKey(recoveryKey);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -743,13 +741,15 @@ class OpenSSSS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _KeyFromPassphraseArgs {
|
class KeyFromPassphraseArgs {
|
||||||
final String passphrase;
|
final String passphrase;
|
||||||
final PassphraseInfo info;
|
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);
|
return await SSSS.keyFromPassphrase(args.passphrase, args.info);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ export 'src/utils/matrix_file.dart';
|
||||||
export 'src/utils/matrix_id_string_extension.dart';
|
export 'src/utils/matrix_id_string_extension.dart';
|
||||||
export 'src/utils/matrix_default_localizations.dart';
|
export 'src/utils/matrix_default_localizations.dart';
|
||||||
export 'src/utils/matrix_localizations.dart';
|
export 'src/utils/matrix_localizations.dart';
|
||||||
|
export 'src/utils/native_implementations.dart';
|
||||||
export 'src/utils/push_notification.dart';
|
export 'src/utils/push_notification.dart';
|
||||||
export 'src/utils/receipt.dart';
|
export 'src/utils/receipt.dart';
|
||||||
export 'src/utils/sync_update_extension.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/extension_recent_emoji/recent_emoji.dart';
|
||||||
export 'msc_extensions/msc_1236_widgets/msc_1236_widgets.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';
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import 'package:matrix/src/utils/sync_update_item_count.dart';
|
||||||
import '../encryption.dart';
|
import '../encryption.dart';
|
||||||
import '../matrix.dart';
|
import '../matrix.dart';
|
||||||
import 'models/timeline_chunk.dart';
|
import 'models/timeline_chunk.dart';
|
||||||
|
import 'utils/compute_callback.dart';
|
||||||
import 'utils/multilock.dart';
|
import 'utils/multilock.dart';
|
||||||
import 'utils/run_benchmarked.dart';
|
import 'utils/run_benchmarked.dart';
|
||||||
|
|
||||||
|
|
@ -90,11 +91,13 @@ class Client extends MatrixApi {
|
||||||
final Map<String, FutureOr<String?> Function(CommandArgs)> commands = {};
|
final Map<String, FutureOr<String?> Function(CommandArgs)> commands = {};
|
||||||
final Filter syncFilter;
|
final Filter syncFilter;
|
||||||
|
|
||||||
|
final NativeImplementations nativeImplementations;
|
||||||
|
|
||||||
String? syncFilterId;
|
String? syncFilterId;
|
||||||
|
|
||||||
final Future<R> Function<Q, R>(FutureOr<R> Function(Q), Q,
|
final ComputeCallback? compute;
|
||||||
{String debugLabel})? compute;
|
|
||||||
|
|
||||||
|
@Deprecated('Use [nativeImplementations] instead')
|
||||||
Future<T> runInBackground<T, U>(
|
Future<T> runInBackground<T, U>(
|
||||||
FutureOr<T> Function(U arg) function, U arg) async {
|
FutureOr<T> Function(U arg) function, U arg) async {
|
||||||
final compute = this.compute;
|
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
|
/// 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
|
/// [supportedLoginTypes]. Set a custom [syncFilter] if you like. By default the app
|
||||||
/// will use lazy_load_members.
|
/// will use lazy_load_members.
|
||||||
/// Set [compute] to the Flutter compute method to enable the SDK to run some
|
/// Set [nativeImplementations] to [NativeImplementationsIsolate] in order to
|
||||||
/// code in background.
|
/// enable the SDK to compute some code in background.
|
||||||
/// Set [timelineEventTimeout] to the preferred time the Client should retry
|
/// Set [timelineEventTimeout] to the preferred time the Client should retry
|
||||||
/// sending events on connection problems or to `Duration.zero` to disable it.
|
/// sending events on connection problems or to `Duration.zero` to disable it.
|
||||||
/// Set [customImageResizer] to your own implementation for a more advanced
|
/// Set [customImageResizer] to your own implementation for a more advanced
|
||||||
|
|
@ -165,7 +168,8 @@ class Client extends MatrixApi {
|
||||||
Set<String>? supportedLoginTypes,
|
Set<String>? supportedLoginTypes,
|
||||||
this.mxidLocalPartFallback = true,
|
this.mxidLocalPartFallback = true,
|
||||||
this.formatLocalpart = true,
|
this.formatLocalpart = true,
|
||||||
this.compute,
|
@Deprecated('Use [nativeImplementations] instead') this.compute,
|
||||||
|
NativeImplementations nativeImplementations = NativeImplementations.dummy,
|
||||||
Level? logLevel,
|
Level? logLevel,
|
||||||
Filter? syncFilter,
|
Filter? syncFilter,
|
||||||
this.sendTimelineEventTimeout = const Duration(minutes: 1),
|
this.sendTimelineEventTimeout = const Duration(minutes: 1),
|
||||||
|
|
@ -182,6 +186,9 @@ class Client extends MatrixApi {
|
||||||
supportedLoginTypes =
|
supportedLoginTypes =
|
||||||
supportedLoginTypes ?? {AuthenticationTypes.password},
|
supportedLoginTypes ?? {AuthenticationTypes.password},
|
||||||
verificationMethods = verificationMethods ?? <KeyVerificationMethod>{},
|
verificationMethods = verificationMethods ?? <KeyVerificationMethod>{},
|
||||||
|
nativeImplementations = compute != null
|
||||||
|
? NativeImplementationsIsolate(compute)
|
||||||
|
: nativeImplementations,
|
||||||
super(
|
super(
|
||||||
httpClient:
|
httpClient:
|
||||||
VariableTimeoutHttpClient(httpClient ?? http.Client())) {
|
VariableTimeoutHttpClient(httpClient ?? http.Client())) {
|
||||||
|
|
@ -227,7 +234,10 @@ class Client extends MatrixApi {
|
||||||
String? _deviceName;
|
String? _deviceName;
|
||||||
|
|
||||||
// for group calls
|
// 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? get groupCallSessionId => _groupCallSessionId;
|
||||||
String? _groupCallSessionId;
|
String? _groupCallSessionId;
|
||||||
|
|
||||||
|
|
@ -2906,5 +2916,6 @@ class HomeserverSummary {
|
||||||
class ArchivedRoom {
|
class ArchivedRoom {
|
||||||
final Room room;
|
final Room room;
|
||||||
final Timeline timeline;
|
final Timeline timeline;
|
||||||
|
|
||||||
ArchivedRoom({required this.room, required this.timeline});
|
ArchivedRoom({required this.room, required this.timeline});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -617,8 +617,8 @@ class Event extends MatrixEvent {
|
||||||
k: fileMap['key']['k'],
|
k: fileMap['key']['k'],
|
||||||
sha256: fileMap['hashes']['sha256'],
|
sha256: fileMap['hashes']['sha256'],
|
||||||
);
|
);
|
||||||
uint8list = await room.client.runInBackground<Uint8List?, EncryptedFile>(
|
uint8list =
|
||||||
decryptFile, encryptedFile);
|
await room.client.nativeImplementations.decryptFile(encryptedFile);
|
||||||
if (uint8list == null) {
|
if (uint8list == null) {
|
||||||
throw ('Unable to decrypt file');
|
throw ('Unable to decrypt file');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -721,7 +721,7 @@ class Room {
|
||||||
FileSendingStatus.generatingThumbnail.name;
|
FileSendingStatus.generatingThumbnail.name;
|
||||||
await _handleFakeSync(syncUpdate);
|
await _handleFakeSync(syncUpdate);
|
||||||
thumbnail ??= await file.generateThumbnail(
|
thumbnail ??= await file.generateThumbnail(
|
||||||
compute: client.runInBackground,
|
nativeImplementations: client.nativeImplementations,
|
||||||
customImageResizer: client.customImageResizer,
|
customImageResizer: client.customImageResizer,
|
||||||
);
|
);
|
||||||
if (shrinkImageMaxDimension != null) {
|
if (shrinkImageMaxDimension != null) {
|
||||||
|
|
@ -730,7 +730,7 @@ class Room {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
maxDimension: shrinkImageMaxDimension,
|
maxDimension: shrinkImageMaxDimension,
|
||||||
customImageResizer: client.customImageResizer,
|
customImageResizer: client.customImageResizer,
|
||||||
compute: client.runInBackground,
|
nativeImplementations: client.nativeImplementations,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -942,7 +942,7 @@ class Room {
|
||||||
? inReplyTo.formattedText
|
? inReplyTo.formattedText
|
||||||
: htmlEscape.convert(inReplyTo.body).replaceAll('\n', '<br>'))
|
: htmlEscape.convert(inReplyTo.body).replaceAll('\n', '<br>'))
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
RegExp(r'<mx-reply>.*<\/mx-reply>',
|
RegExp(r'<mx-reply>.*</mx-reply>',
|
||||||
caseSensitive: false, multiLine: false, dotAll: true),
|
caseSensitive: false, multiLine: false, dotAll: true),
|
||||||
'');
|
'');
|
||||||
final repliedHtml = content.tryGet<String>('formatted_body') ??
|
final repliedHtml = content.tryGet<String>('formatted_body') ??
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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)) !=
|
if (base64.encode(await sha256(input.data)) !=
|
||||||
base64.normalize(input.sha256)) {
|
base64.normalize(input.sha256)) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import 'package:image/image.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
|
|
||||||
import '../../matrix.dart';
|
import '../../matrix.dart';
|
||||||
|
import 'compute_callback.dart';
|
||||||
|
|
||||||
class MatrixFile {
|
class MatrixFile {
|
||||||
final Uint8List bytes;
|
final Uint8List bytes;
|
||||||
|
|
@ -78,15 +79,18 @@ class MatrixImageFile extends MatrixFile {
|
||||||
super(bytes: bytes, name: name, mimeType: mimeType);
|
super(bytes: bytes, name: name, mimeType: mimeType);
|
||||||
|
|
||||||
/// Creates a new image file and calculates the width, height and blurhash.
|
/// Creates a new image file and calculates the width, height and blurhash.
|
||||||
static Future<MatrixImageFile> create(
|
static Future<MatrixImageFile> create({
|
||||||
{required Uint8List bytes,
|
required Uint8List bytes,
|
||||||
required String name,
|
required String name,
|
||||||
String? mimeType,
|
String? mimeType,
|
||||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
|
||||||
compute}) async {
|
NativeImplementations nativeImplementations = NativeImplementations.dummy,
|
||||||
final metaData = compute != null
|
}) async {
|
||||||
? await compute(_calcMetadata, bytes)
|
if (compute != null) {
|
||||||
: _calcMetadata(bytes);
|
nativeImplementations =
|
||||||
|
NativeImplementationsIsolate.fromRunInBackground(compute);
|
||||||
|
}
|
||||||
|
final metaData = await nativeImplementations.calcImageMetadata(bytes);
|
||||||
|
|
||||||
return MatrixImageFile(
|
return MatrixImageFile(
|
||||||
bytes: metaData?.bytes ?? bytes,
|
bytes: metaData?.bytes ?? bytes,
|
||||||
|
|
@ -101,22 +105,27 @@ class MatrixImageFile extends MatrixFile {
|
||||||
/// Builds a [MatrixImageFile] and shrinks it in order to reduce traffic.
|
/// Builds a [MatrixImageFile] and shrinks it in order to reduce traffic.
|
||||||
/// If shrinking does not work (e.g. for unsupported MIME types), the
|
/// If shrinking does not work (e.g. for unsupported MIME types), the
|
||||||
/// initial image is preserved without shrinking it.
|
/// initial image is preserved without shrinking it.
|
||||||
static Future<MatrixImageFile> shrink(
|
static Future<MatrixImageFile> shrink({
|
||||||
{required Uint8List bytes,
|
required Uint8List bytes,
|
||||||
required String name,
|
required String name,
|
||||||
int maxDimension = 1600,
|
int maxDimension = 1600,
|
||||||
String? mimeType,
|
String? mimeType,
|
||||||
Future<MatrixImageFileResizedResponse?> Function(
|
Future<MatrixImageFileResizedResponse?> Function(
|
||||||
MatrixImageFileResizeArguments)?
|
MatrixImageFileResizeArguments)?
|
||||||
customImageResizer,
|
customImageResizer,
|
||||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
|
||||||
compute}) async {
|
NativeImplementations nativeImplementations = NativeImplementations.dummy,
|
||||||
|
}) async {
|
||||||
|
if (compute != null) {
|
||||||
|
nativeImplementations =
|
||||||
|
NativeImplementationsIsolate.fromRunInBackground(compute);
|
||||||
|
}
|
||||||
final image = MatrixImageFile(name: name, mimeType: mimeType, bytes: bytes);
|
final image = MatrixImageFile(name: name, mimeType: mimeType, bytes: bytes);
|
||||||
|
|
||||||
return await image.generateThumbnail(
|
return await image.generateThumbnail(
|
||||||
dimension: maxDimension,
|
dimension: maxDimension,
|
||||||
customImageResizer: customImageResizer,
|
customImageResizer: customImageResizer,
|
||||||
compute: compute) ??
|
nativeImplementations: nativeImplementations) ??
|
||||||
image;
|
image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,6 +150,7 @@ class MatrixImageFile extends MatrixFile {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get msgType => 'm.image';
|
String get msgType => 'm.image';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> get info => ({
|
Map<String, dynamic> get info => ({
|
||||||
...super.info,
|
...super.info,
|
||||||
|
|
@ -151,13 +161,18 @@ class MatrixImageFile extends MatrixFile {
|
||||||
|
|
||||||
/// Computes a thumbnail for the image.
|
/// Computes a thumbnail for the image.
|
||||||
/// Also sets height and width on the original image if they were unset.
|
/// Also sets height and width on the original image if they were unset.
|
||||||
Future<MatrixImageFile?> generateThumbnail(
|
Future<MatrixImageFile?> generateThumbnail({
|
||||||
{int dimension = Client.defaultThumbnailSize,
|
int dimension = Client.defaultThumbnailSize,
|
||||||
Future<MatrixImageFileResizedResponse?> Function(
|
Future<MatrixImageFileResizedResponse?> Function(
|
||||||
MatrixImageFileResizeArguments)?
|
MatrixImageFileResizeArguments)?
|
||||||
customImageResizer,
|
customImageResizer,
|
||||||
Future<T> Function<T, U>(FutureOr<T> Function(U arg) function, U arg)?
|
@Deprecated('Use [nativeImplementations] instead') ComputeRunner? compute,
|
||||||
compute}) async {
|
NativeImplementations nativeImplementations = NativeImplementations.dummy,
|
||||||
|
}) async {
|
||||||
|
if (compute != null) {
|
||||||
|
nativeImplementations =
|
||||||
|
NativeImplementationsIsolate.fromRunInBackground(compute);
|
||||||
|
}
|
||||||
final arguments = MatrixImageFileResizeArguments(
|
final arguments = MatrixImageFileResizeArguments(
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
maxDimension: dimension,
|
maxDimension: dimension,
|
||||||
|
|
@ -166,19 +181,17 @@ class MatrixImageFile extends MatrixFile {
|
||||||
);
|
);
|
||||||
final resizedData = customImageResizer != null
|
final resizedData = customImageResizer != null
|
||||||
? await customImageResizer(arguments)
|
? await customImageResizer(arguments)
|
||||||
: compute != null
|
: await nativeImplementations.shrinkImage(arguments);
|
||||||
? await compute(_resize, arguments)
|
|
||||||
: _resize(arguments);
|
|
||||||
|
|
||||||
if (resizedData == null) {
|
if (resizedData == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we should take the opportinity to update the image dimmension
|
// we should take the opportunity to update the image dimension
|
||||||
setImageSizeIfNull(
|
setImageSizeIfNull(
|
||||||
width: resizedData.originalWidth, height: resizedData.originalHeight);
|
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) {
|
if (resizedData.width > dimension || resizedData.height > dimension) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -194,7 +207,10 @@ class MatrixImageFile extends MatrixFile {
|
||||||
return thumbnailFile;
|
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);
|
final image = decodeImage(bytes);
|
||||||
if (image == null) return null;
|
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) {
|
MatrixImageFileResizeArguments arguments) {
|
||||||
final image = decodeImage(arguments.bytes);
|
final image = decodeImage(arguments.bytes);
|
||||||
|
|
||||||
|
|
@ -255,6 +273,28 @@ class MatrixImageFileResizedResponse {
|
||||||
this.originalWidth,
|
this.originalWidth,
|
||||||
this.blurhash,
|
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 {
|
class MatrixImageFileResizeArguments {
|
||||||
|
|
@ -269,6 +309,21 @@ class MatrixImageFileResizeArguments {
|
||||||
required this.fileName,
|
required this.fileName,
|
||||||
required this.calcBlurhash,
|
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 {
|
class MatrixVideoFile extends MatrixFile {
|
||||||
|
|
@ -284,8 +339,10 @@ class MatrixVideoFile extends MatrixFile {
|
||||||
this.height,
|
this.height,
|
||||||
this.duration})
|
this.duration})
|
||||||
: super(bytes: bytes, name: name, mimeType: mimeType);
|
: super(bytes: bytes, name: name, mimeType: mimeType);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get msgType => 'm.video';
|
String get msgType => 'm.video';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> get info => ({
|
Map<String, dynamic> get info => ({
|
||||||
...super.info,
|
...super.info,
|
||||||
|
|
@ -304,8 +361,10 @@ class MatrixAudioFile extends MatrixFile {
|
||||||
String? mimeType,
|
String? mimeType,
|
||||||
this.duration})
|
this.duration})
|
||||||
: super(bytes: bytes, name: name, mimeType: mimeType);
|
: super(bytes: bytes, name: name, mimeType: mimeType);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get msgType => 'm.audio';
|
String get msgType => 'm.audio';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> get info => ({
|
Map<String, dynamic> get info => ({
|
||||||
...super.info,
|
...super.info,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
Loading…
Reference in New Issue