Skip to content

Commit

Permalink
update persistent storages
Browse files Browse the repository at this point in the history
  • Loading branch information
zhelvis committed Oct 24, 2023
1 parent 9b681ca commit ced0261
Show file tree
Hide file tree
Showing 15 changed files with 282 additions and 295 deletions.
4 changes: 2 additions & 2 deletions packages/tswebextension/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- TODO: manually add compare links for version to the end of the file -->
<!-- e.g. [0.1.2]: https://github.com/AdguardTeam/tsurlfilter/compare/tswebextension-v0.1.1...tswebextension-v0.1.2 -->

## [0.4.2] - 2023-10-17
## [Unreleased]

### Added
- Added new `PersistentMap` and `PersistentValue` utils for restoring data in event-driven background scripts.
- Added new `ExtensionStorage`, `PersistentValueContainer`, `createExtensionStorageDecorator` interfaces and for restoring data in event-driven background scripts.


## [0.4.1] - 2023-10-13
Expand Down
1 change: 1 addition & 0 deletions packages/tswebextension/src/lib/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './content-script/send-app-message';
export * from './request-type';
export * from './error';
export * from './constants';
export * from './storage';
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ExtensionStorage } from './extension-storage';

/**
* Creates accessor decorator for the specified storage.
* @param storage The extension storage API to use.
* @returns Accessor decorator for the specified storage.
* @see https://github.com/tc39/proposal-decorators
*/
export function createExtensionStorageDecorator<Data extends Record<string, unknown>>(
storage: ExtensionStorage<Data, string>,
) {
return function createDecorator<Field extends keyof Data>(field: Field) {
return function decorator<
// The type on which the class element will be defined.
// For a static class element, this will be the type of the constructor.
// For a non-static class element, this will be the type of the instance.
This,
>(
_target: ClassAccessorDecoratorTarget<This, Data[Field]>,
context: ClassAccessorDecoratorContext<This, Data[Field]>,
): ClassAccessorDecoratorResult<This, Data[Field]> | void {
if (context.kind !== 'accessor') {
return undefined;
}

// we do not set init descriptor, because data will be initialized asynchronously
return {
get(): Data[Field] {
return storage.get(field);
},
set(value: Data[Field]): void {
return storage.set(field, value);
},
};
};
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { Storage } from 'webextension-polyfill';

import { PersistentValueContainer } from './persistent-value-container';

/**
* API for storing persistent key-value data with debounced sync with the specified webextension storage key.
* Webextension storage synchronization described in the {@link PersistentValueContainer} class.
*/
export class ExtensionStorage<
Data extends Record<string, unknown>,
Key extends string = string,
> {
/**
* API for storing persistent value with debounced sync with the specified webextension storage key.
*/
#container: PersistentValueContainer<Key, Data>;

/**
* Creates {@link ExtensionStorage} instance.
* @param key The key to use for storing the data.
* @param api Webextension storage API.
*/
constructor(
key: Key,
api: Storage.StorageArea,
) {
this.#container = new PersistentValueContainer<Key, Data>(key, api);
}

/**
* Initializes the storage.
* @param data The initial data.
* @returns Promise that resolves when the storage is initialized.
* @throws Error, if storage already initialized.
*/
init(data: Data): Promise<void> {
return this.#container.init(data);
}

/**
* Gets the value by the specified key.
* @param key The key to use for storing the value.
* @throws Error, if storage not initialized.
* @returns Data stored by the specified key.
*/
get<T extends keyof Data>(key: T): Data[T] {
return this.#container.get()[key];
}

/**
* Sets the value by the specified key.
* @param key The key to use for storing the value.
* @param value New value.
* @throws Error, if storage not initialized.
*/
set<T extends keyof Data>(key: T, value: Data[T]): void {
// TODO: try to avoid cast
const data = this.#container.get();
data[key] = value;
this.#container.set(data);
}

/**
* Deletes the value by the specified key.
* @param key The key to use for storing the value.
* @throws Error, if storage not initialized.
*/
delete(key: keyof Data): void {
const data = this.#container.get();
delete data[key];
this.#container.set(data);
}
}
2 changes: 2 additions & 0 deletions packages/tswebextension/src/lib/common/storage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ExtensionStorage } from './extension-storage';
export { createExtensionStorageDecorator } from './extension-storage-decorator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { debounce } from 'lodash-es';
import browser, { type Storage, type Manifest } from 'webextension-polyfill';

/**
* API to store a persistent value with debounced synchronization to the specified web extension storage key.
*
* After the container is created, we initialize it asynchronously to get the actual value from the storage.
* The Init method is guarded against multiple initializations to avoid unnecessary reads from the memory.
* Get/set methods are protected from uninitialized storage to ensure that actual data is used.
*
* We declare the sync get/set methods to update the cached value. This allows us to use containers in accessors.
*
* Set method updates the cached value and schedules the save operation to the storage via a debounce function to
* avoid unnecessary writes to the storage.
*
* This container saves the data to storage using the specified key to avoid collisions with other instances.
* It helps to avoid reading the data from the storage that is not related to the current instance.
*/
export class PersistentValueContainer<Key extends string = string, Value = unknown> {
static #IS_BACKGROUND_PERSISTENT = PersistentValueContainer.#isBackgroundPersistent();

#api: Storage.StorageArea;

#key: Key;

#value: Value;

// TODO: make required after the migration to event-driven background.
#save?: () => void;

#isInitialized = false;

/**
* Creates {@link PersistentValueContainer} instance.
* @param key The key to use for storing the data.
* @param api Webextension storage API.
* @param debounceMs The debounce time in milliseconds to save the data to the storage.
* Optional. Default is 300ms.
*/
constructor(
key: Key,
api: Storage.StorageArea,
debounceMs = 300,
) {
this.#key = key;
this.#api = api;

/**
* TODO: remove this condition after the migration to event-driven background.
*/
if (!PersistentValueContainer.#IS_BACKGROUND_PERSISTENT) {
this.#save = debounce(() => {
this.#api.set({ [this.#key]: this.#value });
}, debounceMs);
}
}

/**
* Initializes the value.
* @param value The initial value.
* @returns Promise that resolves when the value is initialized.
* @throws Error, if storage already initialized.
*/
async init(value: Value): Promise<void> {
if (this.#isInitialized) {
throw new Error('Storage already initialized');
}

if (PersistentValueContainer.#IS_BACKGROUND_PERSISTENT) {
this.#value = value;
} else {
const storageData = await this.#api.get({
[this.#key]: value,
});

this.#value = storageData[this.#key];
}

this.#isInitialized = true;
}

/**
* Gets the value.
* @returns The value stored by the specified key.
* @throws Error, if storage not initialized.
*/
get(): Value {
this.#checkIsInitialized();

return this.#value;
}

/**
* Sets the value.
* @param value Value to be stored in the specified key.
* @throws Error, if storage not initialized.
*/
set(value: Value): void {
this.#checkIsInitialized();

this.#value = value;

if (this.#save) {
this.#save();
}
}

/**
* Checks if the storage is initialized.
* @throws Error, if storage not initialized.
*/
#checkIsInitialized(): void {
if (!this.#isInitialized) {
throw new Error('Storage not initialized');
}
}

/**
* TODO: remove this method after the migration to event-driven background.
* Checks if the background script is persistent.
* @returns True if the background script is persistent.
*/
static #isBackgroundPersistent(): boolean {
const manifest = browser.runtime.getManifest();

if (manifest.manifest_version === 3) {
return false;
}

if (!manifest.background) {
return true;
}

const background = manifest.background as
(Manifest.WebExtensionManifestBackgroundC2Type | Manifest.WebExtensionManifestBackgroundC1Type);

return background.persistent ?? true;
}
}
3 changes: 0 additions & 3 deletions packages/tswebextension/src/lib/common/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
export * from './channels';
export * from './url';
export * from './logger';
export * from './persistent-map';
export * from './persistent-value';
export * from './is-background-persistent';

This file was deleted.

Loading

0 comments on commit ced0261

Please sign in to comment.