| // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| @JS() | |
| library; | |
| import 'dart:async'; | |
| import 'dart:convert'; | |
| // TODO: https://github.com/dart-lang/webdev/issues/2508 | |
| // ignore: deprecated_member_use | |
| import 'dart:js_util'; | |
| import 'package:dwds/data/data_types.dart'; | |
| import 'package:dwds/data/debug_info.dart'; | |
| // TODO: https://github.com/dart-lang/webdev/issues/2508 | |
| // ignore: deprecated_member_use | |
| import 'package:js/js.dart'; | |
| import 'chrome_api.dart'; | |
| import 'logger.dart'; | |
| import 'utils.dart'; | |
| enum StorageObject { | |
| debugInfo, | |
| devToolsOpener, | |
| devToolsUri, | |
| encodedUri, | |
| isAuthenticated, | |
| multipleAppsDetected; | |
| Persistence get persistence => switch (this) { | |
| StorageObject.debugInfo => Persistence.sessionOnly, | |
| StorageObject.devToolsOpener => Persistence.acrossSessions, | |
| StorageObject.devToolsUri => Persistence.sessionOnly, | |
| StorageObject.encodedUri => Persistence.sessionOnly, | |
| StorageObject.isAuthenticated => Persistence.sessionOnly, | |
| StorageObject.multipleAppsDetected => Persistence.sessionOnly, | |
| }; | |
| } | |
| enum Persistence { sessionOnly, acrossSessions } | |
| Future<bool> setStorageObject<T>({ | |
| required StorageObject type, | |
| required T value, | |
| int? tabId, | |
| void Function()? callback, | |
| }) { | |
| final storageKey = _createStorageKey(type, tabId); | |
| final json = _serialize(value); | |
| final storageObj = <String, String>{storageKey: json}; | |
| final completer = Completer<bool>(); | |
| final storageArea = _getStorageArea(type.persistence); | |
| storageArea.set( | |
| jsify(storageObj), | |
| allowInterop(() { | |
| if (callback != null) { | |
| callback(); | |
| } | |
| debugLog('Set: $json', prefix: storageKey); | |
| completer.complete(true); | |
| }), | |
| ); | |
| return completer.future; | |
| } | |
| String _serialize<T>(T value) { | |
| return switch (value) { | |
| final String v => v, | |
| final DebugInfo v => jsonEncode(v), | |
| final ConnectFailure v => jsonEncode(v), | |
| final DebugStateChange v => jsonEncode(v), | |
| final DevToolsOpener v => jsonEncode(v), | |
| final DevToolsUrl v => jsonEncode(v), | |
| _ => throw UnsupportedError('Unknown type for serialization: $T'), | |
| }; | |
| } | |
| Future<T?> fetchStorageObject<T>({required StorageObject type, int? tabId}) { | |
| final storageKey = _createStorageKey(type, tabId); | |
| final completer = Completer<T?>(); | |
| final storageArea = _getStorageArea(type.persistence); | |
| storageArea.get( | |
| [storageKey], | |
| allowInterop((Object? storageObj) { | |
| if (storageObj == null) { | |
| debugWarn('Does not exist.', prefix: storageKey); | |
| completer.complete(null); | |
| return; | |
| } | |
| final json = getProperty(storageObj, storageKey) as String?; | |
| if (json == null) { | |
| debugWarn('Does not exist.', prefix: storageKey); | |
| completer.complete(null); | |
| } else { | |
| debugLog('Fetched: $json', prefix: storageKey); | |
| completer.complete(_deserialize<T>(json)); | |
| } | |
| }), | |
| ); | |
| return completer.future; | |
| } | |
| T _deserialize<T>(String json) { | |
| if (T == String) return json as T; | |
| final decoded = jsonDecode(json); | |
| return switch (decoded) { | |
| ['DebugInfo', ...] => DebugInfo.fromJson(decoded) as T, | |
| ['ConnectFailure', ...] => ConnectFailure.fromJson(decoded) as T, | |
| ['DebugStateChange', ...] => DebugStateChange.fromJson(decoded) as T, | |
| ['DevToolsOpener', ...] => DevToolsOpener.fromJson(decoded) as T, | |
| ['DevToolsUrl', ...] => DevToolsUrl.fromJson(decoded) as T, | |
| _ => throw UnsupportedError('Unknown type for deserialization: $T'), | |
| }; | |
| } | |
| Future<List<T>> fetchAllStorageObjectsOfType<T>({required StorageObject type}) { | |
| final completer = Completer<List<T>>(); | |
| final storageArea = _getStorageArea(type.persistence); | |
| storageArea.get( | |
| null, | |
| allowInterop((Object? storageContents) { | |
| if (storageContents == null) { | |
| debugWarn('No storage objects of type exist.', prefix: type.name); | |
| completer.complete([]); | |
| return; | |
| } | |
| final allKeys = List<String>.from(objectKeys(storageContents)); | |
| final storageKeys = allKeys.where((key) => key.contains(type.name)); | |
| final result = <T>[]; | |
| for (final key in storageKeys) { | |
| final json = getProperty(storageContents, key) as String?; | |
| if (json != null) { | |
| result.add(_deserialize<T>(json)); | |
| } | |
| } | |
| completer.complete(result); | |
| }), | |
| ); | |
| return completer.future; | |
| } | |
| Future<bool> removeStorageObject({required StorageObject type, int? tabId}) { | |
| final storageKey = _createStorageKey(type, tabId); | |
| final completer = Completer<bool>(); | |
| final storageArea = _getStorageArea(type.persistence); | |
| storageArea.remove( | |
| [storageKey], | |
| allowInterop(() { | |
| debugLog('Removed object.', prefix: storageKey); | |
| completer.complete(true); | |
| }), | |
| ); | |
| return completer.future; | |
| } | |
| void interceptStorageChange<T>({ | |
| required Object storageObj, | |
| required StorageObject expectedType, | |
| required void Function(T? storageObj) changeHandler, | |
| int? tabId, | |
| }) { | |
| try { | |
| final expectedStorageKey = _createStorageKey(expectedType, tabId); | |
| final isExpected = hasProperty(storageObj, expectedStorageKey); | |
| if (!isExpected) return; | |
| final objProp = getProperty(storageObj, expectedStorageKey); | |
| final json = getProperty(objProp, 'newValue') as String?; | |
| final decodedObj = json != null ? _deserialize<T>(json) : null; | |
| debugLog('Intercepted $expectedStorageKey change: $json'); | |
| return changeHandler(decodedObj); | |
| } catch (error) { | |
| debugError( | |
| 'Error intercepting storage object with type $expectedType: $error', | |
| ); | |
| } | |
| } | |
| StorageArea _getStorageArea(Persistence persistence) { | |
| // MV2 extensions don't have access to session storage: | |
| if (!isMV3) return chrome.storage.local; | |
| return switch (persistence) { | |
| Persistence.acrossSessions => chrome.storage.local, | |
| Persistence.sessionOnly => chrome.storage.session, | |
| }; | |
| } | |
| String _createStorageKey(StorageObject type, int? tabId) { | |
| if (tabId == null) return type.name; | |
| return '$tabId-${type.name}'; | |
| } |