-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
282 additions
and
295 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
packages/tswebextension/src/lib/common/storage/extension-storage-decorator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}, | ||
}; | ||
}; | ||
}; | ||
} |
73 changes: 73 additions & 0 deletions
73
packages/tswebextension/src/lib/common/storage/extension-storage.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
139 changes: 139 additions & 0 deletions
139
packages/tswebextension/src/lib/common/storage/persistent-value-container.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
22 changes: 0 additions & 22 deletions
22
packages/tswebextension/src/lib/common/utils/is-background-persistent.ts
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.