Skip to content

Commit

Permalink
refactor: replace pouchdb indexeddb storage with extension storage
Browse files Browse the repository at this point in the history
  • Loading branch information
mkazlauskas committed Dec 6, 2024
1 parent cf85d88 commit cf057df
Show file tree
Hide file tree
Showing 9 changed files with 434 additions and 26 deletions.
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);
}
}
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(

Check failure on line 52 in apps/browser-extension-wallet/src/lib/scripts/background/storage/extension-blob-key-value-store.ts

View workflow job for this annotation

GitHub Actions / Release package

Delete `⏎········`
(collection, { key, value }) => {
collection[key] = value;

Check failure on line 54 in apps/browser-extension-wallet/src/lib/scripts/background/storage/extension-blob-key-value-store.ts

View workflow job for this annotation

GitHub Actions / Release package

Delete `··`
return collection;

Check failure on line 55 in apps/browser-extension-wallet/src/lib/scripts/background/storage/extension-blob-key-value-store.ts

View workflow job for this annotation

GitHub Actions / Release package

Replace `··········` with `········`
},

Check failure on line 56 in apps/browser-extension-wallet/src/lib/scripts/background/storage/extension-blob-key-value-store.ts

View workflow job for this annotation

GitHub Actions / Release package

Replace `········},⏎········{}·as·Record<K,·V>⏎······` with `······},·{}·as·Record<K,·V>`
{} as Record<K, V>
)
);
}
}
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

View workflow job for this annotation

GitHub Actions / Release package

Replace `⏎····protected·docId:·string,⏎····logger:·Logger⏎··` with `protected·docId:·string,·logger:·Logger`
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);
}
}
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()
);
}
}
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;
};
Loading

0 comments on commit cf057df

Please sign in to comment.