| // 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'; |
| |
| // TODO: https://github.com/dart-lang/webdev/issues/2508 |
| // ignore: deprecated_member_use |
| import 'package:js/js.dart'; |
| |
| import 'chrome_api.dart'; |
| import 'data_serializers.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 = |
| value is String ? value : jsonEncode(serializers.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; |
| } |
| |
| 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); |
| if (T == String) { |
| completer.complete(json as T); |
| } else { |
| final value = serializers.deserialize(jsonDecode(json)) as T; |
| completer.complete(value); |
| } |
| } |
| }), |
| ); |
| return completer.future; |
| } |
| |
| 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) { |
| if (T == String) { |
| result.add(json as T); |
| } else { |
| result.add(serializers.deserialize(jsonDecode(json)) as T); |
| } |
| } |
| } |
| 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?; |
| T? decodedObj; |
| if (json == null || T == String) { |
| decodedObj = json as T?; |
| } else { |
| decodedObj = serializers.deserialize(jsonDecode(json)) as T?; |
| } |
| 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}'; |
| } |