Skip to content
This repository has been archived by the owner on Sep 5, 2023. It is now read-only.

Commit

Permalink
Merge pull request #73 from mobilejazz/feature/improve-storage-dataso…
Browse files Browse the repository at this point in the history
…urce

Improve StorageDataSource
  • Loading branch information
doup authored Nov 12, 2021
2 parents 05a6e45 + 0d44957 commit 57799e0
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 65 deletions.
3 changes: 2 additions & 1 deletion packages/core/src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './url-builder';
export * from './safe-storage';
export * from './string.utils';
export * from './url-builder';
export * from './logger/logger';
export * from './logger/device-console.logger';
export * from './logger/void.logger';
77 changes: 77 additions & 0 deletions packages/core/src/helpers/safe-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* SafeStorage
*
* Wraps any `Storage` implementation so in case that the `Storage` is not available
* it uses an in memory implementation. Check the following article for more info on
* why this is needed:
* https://michalzalecki.com/why-using-localStorage-directly-is-a-bad-idea/
*/
export class SafeStorage implements Storage {
private readonly isSupported: boolean;
private inMemoryStorage: Record<string, string> = {};

constructor (
private readonly storage: Storage,
) {
try {
const testKey = 'gjsLwbKbR3rk7xqTKWt3iVHA9hoJHsyVcC9M6wNF';
this.storage.setItem(testKey, testKey);
this.storage.removeItem(testKey);
this.isSupported = true;
} catch (e) {
this.isSupported = false;
}
}

public clear(): void {
if (this.isSupported) {
this.storage.clear();
} else {
this.inMemoryStorage = {};
}
}

// eslint-disable-next-line @typescript-eslint/ban-types
public getItem(key: string): string | null {
if (this.isSupported) {
return this.storage.getItem(key);
} else if (Object.prototype.hasOwnProperty.call(this.inMemoryStorage, key)) {
return this.inMemoryStorage[key];
}

return null;
}

// eslint-disable-next-line @typescript-eslint/ban-types
public key(index: number): string | null {
if (this.isSupported) {
return this.storage.key(index);
} else {
return Object.keys(this.inMemoryStorage)[index] ?? null;
}
}

public removeItem(key: string): void {
if (this.isSupported) {
this.storage.removeItem(key);
} else {
delete this.inMemoryStorage[key];
}
}

public setItem(key: string, value: string): void {
if (this.isSupported) {
this.storage.setItem(key, value);
} else {
this.inMemoryStorage[key] = String(value);
}
}

public get length(): number {
if (this.isSupported) {
return this.storage.length;
} else {
return Object.keys(this.inMemoryStorage).length;
}
}
}

This file was deleted.

112 changes: 112 additions & 0 deletions packages/core/src/repository/data-source/storage.data-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { DeleteError, FailedError, NotFoundError, KeyListQuery, KeyQuery, Query, QueryNotSupportedError, InvalidArgumentError } from '..';
import { DeleteDataSource, GetDataSource, PutDataSource } from './data-source';
import { Logger, SafeStorage, VoidLogger } from '../../helpers';

export class StorageDataSource implements GetDataSource<string>, PutDataSource<string>, DeleteDataSource {
private readonly storage: Storage;

/**
* @param storage Any instance of `Storage`, usually `localStorage` or `sessionStorage`
* @param enableSafeMode Wrap the given `storage` in `SafeStorage`. This prevents errors in incognito and permission-less scenarios. Keep in mind that `SafeStorage` fallbacks to an **in-memory implementation**. If you need more control on how to handle incognito/permission issues then you should set this to `false` and handle these issues in a Repository. More info: https://michalzalecki.com/why-using-localStorage-directly-is-a-bad-idea/
* @param logger Logger instance, defaults to `VoidLogger` (no logger)
*/
constructor(
storage: Storage,
enableSafeMode: boolean,
private readonly logger: Logger = new VoidLogger(),
) {
this.storage = enableSafeMode ? new SafeStorage(storage) : storage;
}

private getItem(key: string): string {
const item = this.storage.getItem(key);

if (item === null) {
throw new NotFoundError(`"${key}" not found in Storage`);
}

return item;
}

private getKeys(query: KeyQuery | KeyListQuery): string[] {
let keys: string[];

if (query instanceof KeyListQuery) {
keys = query.keys;
} else {
keys = query.key.split(',');
}

return keys;
}

private setItem(key: string, value: string): void {
try {
this.storage.setItem(key, value);
} catch (err) {
// Handle `QuotaExceededError` (or others), with a generic Harmony error
throw new FailedError(`Error while saving in Storage`);
}
}

public async get(query: Query): Promise<string> {
if (query instanceof KeyQuery) {
return this.getItem(query.key);
} else {
throw new QueryNotSupportedError();
}
}

public async getAll(query: Query): Promise<string[]> {
if (query instanceof KeyQuery || query instanceof KeyListQuery) {
const keys = this.getKeys(query);
return keys.map((key) => this.getItem(key));
} else {
throw QueryNotSupportedError;
}
}

public async put(value: string, query: Query): Promise<string> {
if (query instanceof KeyQuery) {
this.setItem(query.key, value);
return this.getItem(query.key);
} else {
throw QueryNotSupportedError;
}
}

public async putAll(values: string[], query: Query): Promise<string[]> {
if (query instanceof KeyQuery || query instanceof KeyListQuery) {
const keys = this.getKeys(query);

if (values.length !== keys.length) {
throw new InvalidArgumentError(`Values lengh (${values.length}) and keys length (${keys.length}) don't match.`);
}

return keys.map((key, index) => {
this.setItem(key, values[index]);
return this.getItem(key);
});
} else {
throw QueryNotSupportedError;
}
}

public async delete(query: Query): Promise<void> {
if (query instanceof KeyQuery || query instanceof KeyListQuery) {
const keys = this.getKeys(query);
const results = keys.map<boolean>((key) => {
this.storage.removeItem(key);
return this.storage.getItem(key) === null;
});

if (results.indexOf(false) !== -1) {
throw new DeleteError();
}

return;
} else {
throw QueryNotSupportedError;
}
}
}
2 changes: 1 addition & 1 deletion packages/core/src/repository/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from './data-source/data-source';
export * from './data-source/data-source-mapper';
export * from './data-source/local-storage.data-source';
export * from './data-source/storage.data-source';
export * from './data-source/void.data-source';
export * from './data-source/in-memory.data-source';
export * from './errors';
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/repository/query/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export class KeyQuery extends Query {
}
}

export class KeyListQuery extends KeyQuery {
constructor(public readonly keys: string[]) {
super(keys.join(','));
}
}

export class VoidQuery extends Query {}

export class IdQuery<T extends number | string> extends KeyQuery {
Expand Down

0 comments on commit 57799e0

Please sign in to comment.