Skip to content

Commit

Permalink
Pull request #695: AG-19767 Migrate Firefox add-on to event pages
Browse files Browse the repository at this point in the history
Merge in ADGUARD-FILTERS/tsurlfilter from feature/AG-19767 to master

Squashed commit of the following:

commit 8916997
Merge: b028a50 a1234dc
Author: Vladimir Zhelvis <[email protected]>
Date:   Wed Nov 1 13:13:27 2023 +0300

    Merge branch 'master' into feature/AG-19767

commit b028a50
Author: Vladimir Zhelvis <[email protected]>
Date:   Wed Nov 1 12:25:31 2023 +0300

    update docs

commit 7a21a1e
Author: Vladimir Zhelvis <[email protected]>
Date:   Wed Nov 1 12:06:16 2023 +0300

    update tests

commit f91948a
Author: Vladimir Zhelvis <[email protected]>
Date:   Fri Oct 27 15:43:32 2023 +0300

    delete deprecated todo

commit 789e09a
Author: Vladimir Zhelvis <[email protected]>
Date:   Thu Oct 26 20:35:22 2023 +0300

    update storage decorator

commit d972184
Author: Vladimir Zhelvis <[email protected]>
Date:   Wed Oct 25 12:41:51 2023 +0300

    update comment

commit 5c8087e
Author: Vladimir Zhelvis <[email protected]>
Date:   Wed Oct 25 12:40:12 2023 +0300

    update comments

commit d801043
Author: Vladimir Zhelvis <[email protected]>
Date:   Tue Oct 24 20:44:13 2023 +0300

    update changelog

commit 587e8f2
Merge: ced0261 6a209c6
Author: Vladimir Zhelvis <[email protected]>
Date:   Tue Oct 24 20:32:39 2023 +0300

    update persistent storages

commit ced0261
Author: Vladimir Zhelvis <[email protected]>
Date:   Tue Oct 24 20:29:50 2023 +0300

    update persistent storages

commit 9b681ca
Author: Vladimir Zhelvis <[email protected]>
Date:   Tue Oct 17 12:56:07 2023 +0300

    bump version

commit fb6b6f0
Author: Vladimir Zhelvis <[email protected]>
Date:   Tue Oct 17 12:55:50 2023 +0300

    update changelog

commit e22cda5
Author: Vladimir Zhelvis <[email protected]>
Date:   Tue Oct 17 12:55:26 2023 +0300

    update rollup config

commit 0809fce
Author: Vladimir Zhelvis <[email protected]>
Date:   Tue Oct 17 12:02:33 2023 +0300

    fix PersistentMap util

commit 9a35ea4
Merge: a3b3965 1f1fe7e
Author: Vladimir Zhelvis <[email protected]>
Date:   Tue Oct 17 12:01:50 2023 +0300

    Merge branch 'master' into feature/AG-19767

commit a3b3965
Author: Maxim Topciu <[email protected]>
Date:   Fri Oct 13 13:26:00 2023 +0300

    AG-19767 fix tests

commit 6ba4410
Author: Vladimir Zhelvis <[email protected]>
Date:   Fri Oct 13 10:54:21 2023 +0300

    add persistent stores
  • Loading branch information
zhelvis committed Nov 1, 2023
1 parent a1234dc commit f0fd86d
Show file tree
Hide file tree
Showing 27 changed files with 554 additions and 16 deletions.
11 changes: 11 additions & 0 deletions packages/tswebextension/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true
},
"transform": {
"decoratorVersion": "2022-03"
}
}
}
6 changes: 6 additions & 0 deletions packages/tswebextension/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ 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 -->

## [Unreleased]

### Added
- Added new `ExtensionStorage`, `PersistentValueContainer`, `createExtensionStorageDecorator` interfaces and for restoring data in event-driven background scripts [#2286](https://github.com/AdguardTeam/AdguardBrowserExtension/issues/2286).


## [0.4.2] - 2023-10-17

### Fixed
Expand Down
3 changes: 3 additions & 0 deletions packages/tswebextension/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const config: Config = {
transform: {
'.+\\.(js|ts)': '@swc/jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es)/)',
],
testEnvironment: 'jsdom',
testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$',
moduleFileExtensions: [
Expand Down
4 changes: 3 additions & 1 deletion packages/tswebextension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"bowser": "2.11.0",
"commander": "11.0.0",
"fs-extra": "11.1.1",
"lodash-es": "^4.17.21",
"lru_map": "0.4.1",
"nanoid": "4.0.2",
"text-encoding": "git+https://github.com/AdguardTeam/text-encoding.git#v0.7.2",
Expand All @@ -106,8 +107,9 @@
"@types/chrome": "^0.0.237",
"@types/fs-extra": "^11.0.1",
"@types/jest": "29.5.2",
"@types/lodash-es": "^4.17.9",
"@types/sinon-chrome": "2.2.11",
"@types/webextension-polyfill": "0.10.0",
"@types/webextension-polyfill": "^0.10.4",
"@typescript-eslint/eslint-plugin": "5.59.11",
"@typescript-eslint/parser": "5.59.11",
"coveralls": "3.1.1",
Expand Down
1 change: 1 addition & 0 deletions packages/tswebextension/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const backgroundMv2Config = {
'deepmerge',
'nanoid',
'lru_map',
'lodash-es',
],
plugins: [
...commonPlugins,
Expand Down
2 changes: 2 additions & 0 deletions packages/tswebextension/setupTests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import browser from 'sinon-chrome';

browser.runtime.getManifest.returns({ version: '2', manifest_version: 2 });

jest.mock('webextension-polyfill', () => ({
...browser,
webRequest: {
Expand Down
7 changes: 4 additions & 3 deletions packages/tswebextension/src/cli/copyWar.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
/* eslint-disable no-console */
import path from 'path';
import { copy } from 'fs-extra';
import { logger } from '../lib/common/utils/logger';

const REDIRECTS_CONFIG_PATH = 'redirects.yml';
const REDIRECTS_RESOURCES_SRC_PATH = 'redirect-files';
const REDIRECTS_RESOURCES_DEST_PATH = 'redirects';

const src = path.resolve(require.resolve('@adguard/scriptlets'), '../..');

// TODO: use logger from lib after import fix
export const copyWar = async (dest: string): Promise<void> => {
dest = path.resolve(process.cwd(), dest);

try {
await copy(path.resolve(src, REDIRECTS_CONFIG_PATH), path.resolve(dest, REDIRECTS_CONFIG_PATH));
await copy(path.resolve(src, REDIRECTS_RESOURCES_SRC_PATH), path.resolve(dest, REDIRECTS_RESOURCES_DEST_PATH));

logger.info(`Web accessible resources was copied to ${dest}`);
console.info(`Web accessible resources was copied to ${dest}`);
} catch (e) {
logger.error((e as Error).message);
console.error((e as Error).message);
}
};
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,61 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
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>,
) {
const fields = new Set<keyof Data>();

/**
* Creates accessor decorator for the specified storage field.
*
* NOTE: You should not set the initial value to the accessor via assignment,
* because decorator overwrite accessors methods and don't use private property, created on initialization.
* Use Non-null assertion operator instead.
* @example `@storage('foo') accessor bar!: string`;
* @param field Storage field name.
* @throws Error if decorator is already registered for {@link field}
* or decorator is applied to class member different from auto accessor.
* @returns Decorator for access to specified storage {@link field}.
*/
return function createFieldDecorator<Field extends keyof Data>(field: Field) {
// We prevent the use of multiple decorators on a single storage field,
// because manipulating data through the accessors of multiple modules can be confusing.
if (fields.has(field)) {
throw new Error(`Decorator for ${String(field)} field is already registered`);
}

fields.add(field);

return function fieldDecorator<
// 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') {
throw new Error('Class member is not auto accessor');
}

// 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,72 @@
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 {
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);
}
}
3 changes: 3 additions & 0 deletions packages/tswebextension/src/lib/common/storage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { PersistentValueContainer } from './persistent-value-container';
export { ExtensionStorage } from './extension-storage';
export { createExtensionStorageDecorator } from './extension-storage-decorator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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> {
// TODO: delete after the migration to event-driven background.
// We do not recalculate this value because the background type cannot change at runtime.
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: 3 additions & 0 deletions packages/tswebextension/src/lib/mv2/background/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable class-methods-use-this */
import { sessionStorage } from './session-storage';
import { appContext } from './context';
import { WebRequestApi } from './web-request-api';
import {
Expand Down Expand Up @@ -117,6 +118,8 @@ MessageHandlerMV2
* @throws Error if configuration is not valid.
*/
public async start(configuration: ConfigurationMV2): Promise<void> {
await sessionStorage.init();

configurationMV2Validator.parse(configuration);

this.configuration = TsWebExtension.createConfigurationMV2Context(configuration);
Expand Down
Loading

0 comments on commit f0fd86d

Please sign in to comment.