-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: replace pouchdb indexeddb storage with extension storage
- Loading branch information
1 parent
cf85d88
commit cf057df
Showing
9 changed files
with
434 additions
and
26 deletions.
There are no files selected for viewing
39 changes: 39 additions & 0 deletions
39
...er-extension-wallet/src/lib/scripts/background/storage/extension-blob-collection-store.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,39 @@ | ||
/* eslint-disable @typescript-eslint/ban-types, brace-style */ | ||
import { ExtensionDocumentStore } from './extension-document-store'; | ||
import { storage as sdkStorage } from '@cardano-sdk/wallet'; | ||
import { Logger } from 'ts-log'; | ||
import { concat, defaultIfEmpty, EMPTY, filter, map, mergeMap, Observable, of } from 'rxjs'; | ||
import { isNotNil } from '@cardano-sdk/util'; | ||
|
||
/** | ||
* Stores entire collection in a single document | ||
*/ | ||
export class ExtensionBlobCollectionStore<T extends {}> | ||
extends ExtensionDocumentStore<T[]> | ||
implements sdkStorage.CollectionStore<T> | ||
{ | ||
/** | ||
* @param collectionName used as extension storage key | ||
*/ | ||
constructor(collectionName: string, logger: Logger) { | ||
super(collectionName, logger); | ||
} | ||
|
||
observeAll(): Observable<T[]> { | ||
return concat( | ||
this.get().pipe(defaultIfEmpty([])), | ||
this.documentChange$.pipe( | ||
map(({ newValue }) => newValue), | ||
filter(isNotNil) | ||
) | ||
); | ||
} | ||
|
||
getAll(): Observable<T[]> { | ||
return this.get().pipe(mergeMap((items) => (items.length > 0 ? of(items) : EMPTY))); | ||
} | ||
|
||
setAll(docs: T[]): Observable<void> { | ||
return this.set(docs); | ||
} | ||
} |
61 changes: 61 additions & 0 deletions
61
...ser-extension-wallet/src/lib/scripts/background/storage/extension-blob-key-value-store.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,61 @@ | ||
/* eslint-disable @typescript-eslint/ban-types, brace-style */ | ||
import { ExtensionDocumentStore } from './extension-document-store'; | ||
import { storage as sdkStorage } from '@cardano-sdk/wallet'; | ||
import { Logger } from 'ts-log'; | ||
import { EMPTY, mergeMap, Observable, of } from 'rxjs'; | ||
import { OpaqueString } from '@cardano-sdk/util'; | ||
|
||
/** | ||
* Stores entire key-value collection in a single document | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export class ExtensionBlobKeyValueStore<K extends string | OpaqueString<any>, V extends {}> | ||
extends ExtensionDocumentStore<Record<K, V>> | ||
implements sdkStorage.KeyValueStore<K, V> | ||
{ | ||
/** | ||
* @param collectionName used as extension storage key | ||
*/ | ||
constructor(collectionName: string, logger: Logger) { | ||
super(collectionName, logger); | ||
} | ||
|
||
getValues(keys: K[]): Observable<V[]> { | ||
return this.get().pipe( | ||
mergeMap((collection): Observable<V[]> => { | ||
const values: V[] = []; | ||
for (const key of keys) { | ||
const value = collection[key]; | ||
if (!value) { | ||
this.logger.debug(`Key "$${key}" was not found`); | ||
return EMPTY; | ||
} | ||
} | ||
return of(values); | ||
}) | ||
); | ||
} | ||
|
||
setValue(key: K, value: V): Observable<void> { | ||
return this.get().pipe( | ||
mergeMap((collection) => | ||
this.set({ | ||
...collection, | ||
[key]: value | ||
}) | ||
) | ||
); | ||
} | ||
|
||
setAll(docs: sdkStorage.KeyValueCollection<K, V>[]): Observable<void> { | ||
return this.set( | ||
docs.reduce( | ||
(collection, { key, value }) => { | ||
collection[key] = value; | ||
return collection; | ||
}, | ||
{} as Record<K, V> | ||
) | ||
); | ||
} | ||
} |
74 changes: 74 additions & 0 deletions
74
apps/browser-extension-wallet/src/lib/scripts/background/storage/extension-document-store.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,74 @@ | ||
/* eslint-disable @typescript-eslint/ban-types */ | ||
import { storage as sdkStorage } from '@cardano-sdk/wallet'; | ||
import { EMPTY, filter, from, map, mergeMap, Observable, of, share } from 'rxjs'; | ||
import { Logger } from 'ts-log'; | ||
import { contextLogger, fromSerializableObject, toSerializableObject } from '@cardano-sdk/util'; | ||
import { ExtensionStore } from './extension-store'; | ||
|
||
export type DocumentChange<T> = { | ||
oldValue?: T; | ||
newValue?: T; | ||
}; | ||
|
||
const undefinedIfEmpty = <T>(value: T | undefined): T | undefined => { | ||
if (typeof value === 'object' && (value === null || Object.keys(value).length === 0)) return undefined; | ||
// eslint-disable-next-line consistent-return | ||
return value; | ||
}; | ||
|
||
export class ExtensionDocumentStore<T extends {}> extends ExtensionStore implements sdkStorage.DocumentStore<T> { | ||
// TODO: remove this when moving the interface to lace-platform,, it's not used | ||
public destroyed: boolean; | ||
|
||
protected documentChange$: Observable<DocumentChange<T>>; | ||
|
||
// used to serialize the writes | ||
private idle: Promise<void> = Promise.resolve(); | ||
|
||
/** | ||
* @param docId unique document id within the store, used as extension storage key | ||
*/ | ||
constructor( | ||
Check failure on line 31 in apps/browser-extension-wallet/src/lib/scripts/background/storage/extension-document-store.ts GitHub Actions / Release package
|
||
protected docId: string, | ||
logger: Logger | ||
) { | ||
super(contextLogger(logger, `ExtensionStore(${docId})`)); | ||
this.documentChange$ = this.storageChange$.pipe( | ||
filter(({ key }) => key === docId), | ||
map( | ||
({ change }): DocumentChange<T> => ({ | ||
oldValue: undefinedIfEmpty(change.oldValue), | ||
newValue: undefinedIfEmpty(change.newValue) | ||
}) | ||
), | ||
share() | ||
); | ||
} | ||
|
||
get(): Observable<T> { | ||
return from(this.storage.get(this.docId)).pipe( | ||
mergeMap((values) => { | ||
const value = values[this.docId]; | ||
this.logger.debug('get', value); | ||
return value ? of(fromSerializableObject(value)) : EMPTY; | ||
}) | ||
); | ||
} | ||
|
||
set(doc: T): Observable<void> { | ||
this.logger.debug('set', doc); | ||
return from( | ||
(this.idle = this.idle.then(() => | ||
this.storage.set({ | ||
[this.docId]: toSerializableObject(doc) | ||
}) | ||
)) | ||
); | ||
} | ||
|
||
destroy(): Observable<void> { | ||
this.destroyed = true; | ||
// eslint-disable-next-line unicorn/no-null | ||
return this.set(null); | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
apps/browser-extension-wallet/src/lib/scripts/background/storage/extension-store.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,20 @@ | ||
/* eslint-disable @typescript-eslint/ban-types */ | ||
import { storage as extensionStorage, Storage } from 'webextension-polyfill'; | ||
import { fromEventPattern, mergeMap, Observable, share } from 'rxjs'; | ||
import { Logger } from 'ts-log'; | ||
|
||
export abstract class ExtensionStore { | ||
protected readonly storageChange$: Observable<{ key: string; change: Storage.StorageChange }>; | ||
protected readonly storage: Storage.StorageArea; | ||
|
||
constructor(protected logger: Logger) { | ||
this.storage = extensionStorage.local; | ||
this.storageChange$ = fromEventPattern<Storage.StorageAreaOnChangedChangesType>( | ||
(handler) => this.storage.onChanged.addListener(handler), | ||
(handler) => this.storage.onChanged.removeListener(handler) | ||
).pipe( | ||
mergeMap((changes) => Object.entries(changes).map(([key, change]) => ({ key, change }))), | ||
share() | ||
); | ||
} | ||
} |
109 changes: 109 additions & 0 deletions
109
apps/browser-extension-wallet/src/lib/scripts/background/storage/migrations.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,109 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types */ | ||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types, unicorn/no-null */ | ||
import { contextLogger } from '@cardano-sdk/util'; | ||
import { storage } from '@cardano-sdk/wallet'; | ||
import { defaultIfEmpty, firstValueFrom, map, Observable } from 'rxjs'; | ||
import { Logger } from 'ts-log'; | ||
|
||
const migrateStore = async <T, Store>( | ||
fromStore: Store, | ||
toStore: Store, | ||
get: (store: Store) => Observable<T | null>, | ||
set: (store: Store, value: T) => Observable<void>, | ||
logger: Logger | ||
// eslint-disable-next-line max-params | ||
) => { | ||
const toValue = await firstValueFrom(get(toStore)); | ||
if (toValue) { | ||
logger.debug('Skip store migration: already migrated'); | ||
return false; | ||
} | ||
const fromValue = await firstValueFrom(get(fromStore)); | ||
if (!fromValue) { | ||
logger.debug('Skip store migration: no data found in source'); | ||
return false; | ||
} | ||
await firstValueFrom(set(toStore, fromValue)); | ||
logger.info('Migrated store'); | ||
return true; | ||
}; | ||
|
||
const migrateDocumentStore = async <T>( | ||
fromStore: storage.DocumentStore<T>, | ||
toStore: storage.DocumentStore<T>, | ||
logger: Logger | ||
) => | ||
migrateStore( | ||
fromStore, | ||
toStore, | ||
(store) => store.get().pipe(defaultIfEmpty<T, null>(null)), | ||
(store, value) => store.set(value), | ||
logger | ||
); | ||
|
||
export const migrateCollectionStore = async <T>( | ||
fromStore: storage.CollectionStore<T>, | ||
toStore: storage.CollectionStore<T>, | ||
logger: Logger | ||
) => | ||
migrateStore( | ||
fromStore, | ||
toStore, | ||
(store) => | ||
store.getAll().pipe( | ||
map((items) => (items.length > 0 ? items : null)), | ||
defaultIfEmpty<T[], null>(null) | ||
), | ||
(store, items) => store.setAll(items), | ||
logger | ||
); | ||
|
||
export const shouldAttemptWalletStoresMigration = async (stores: storage.WalletStores) => { | ||
const tip = await firstValueFrom(stores.tip.get().pipe(defaultIfEmpty(null))); | ||
return !tip; | ||
}; | ||
|
||
export const migrateWalletStores = async ( | ||
fromStores: storage.WalletStores, | ||
toStores: storage.WalletStores, | ||
baseLogger: Logger | ||
): Promise<boolean> => { | ||
const logger = contextLogger(baseLogger, 'StorageMigration'); | ||
if (!(await migrateDocumentStore(fromStores.tip, toStores.tip, contextLogger(logger, 'tip')))) { | ||
logger.debug('Appears to be already migrated'); | ||
return false; | ||
} | ||
await migrateDocumentStore( | ||
fromStores.protocolParameters, | ||
toStores.protocolParameters, | ||
contextLogger(logger, 'protocolParameters') | ||
); | ||
await migrateDocumentStore( | ||
fromStores.genesisParameters, | ||
toStores.genesisParameters, | ||
contextLogger(logger, 'genesisParameters') | ||
); | ||
await migrateDocumentStore(fromStores.eraSummaries, toStores.eraSummaries, contextLogger(logger, 'eraSummaries')); | ||
await migrateDocumentStore(fromStores.assets, toStores.assets, contextLogger(logger, 'assets')); | ||
await migrateDocumentStore(fromStores.addresses, toStores.addresses, contextLogger(logger, 'addresses')); | ||
await migrateDocumentStore( | ||
fromStores.inFlightTransactions, | ||
toStores.inFlightTransactions, | ||
contextLogger(logger, 'inFlightTransactions') | ||
); | ||
await migrateDocumentStore( | ||
fromStores.volatileTransactions, | ||
toStores.volatileTransactions, | ||
contextLogger(logger, 'volatileTransactions') | ||
); | ||
await migrateDocumentStore(fromStores.policyIds, toStores.policyIds, contextLogger(logger, 'policyIds')); | ||
await migrateDocumentStore( | ||
fromStores.signedTransactions, | ||
toStores.signedTransactions, | ||
contextLogger(logger, 'signedTransactions') | ||
); | ||
|
||
await migrateCollectionStore(fromStores.transactions, toStores.transactions, contextLogger(logger, 'transactions')); | ||
await migrateCollectionStore(fromStores.utxo, toStores.utxo, contextLogger(logger, 'utxo')); | ||
return true; | ||
}; |
Oops, something went wrong.