Skip to content

Commit

Permalink
feat(toolkit/storage): allow synchronous retrieval of an item
Browse files Browse the repository at this point in the history
```ts
   const storage = new WebStorage(window.sessionStorage);
   const item = storage.get('key');
```
  • Loading branch information
danielwiehl committed Mar 9, 2022
1 parent 063fc38 commit 4965ed8
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 32 deletions.
67 changes: 62 additions & 5 deletions projects/scion/toolkit/storage/src/web-storage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,32 +201,89 @@ describe('WebStorage', () => {
});
});

describe('WebStorage.get', () => {

it('should return the value associated with the given key', () => {
const testee = new WebStorage(window.sessionStorage);
testee.put('key', 'value');
expect(testee.get('key')).toEqual('value');
});

it('should return the object associated with the given key', () => {
const testee = new WebStorage(window.sessionStorage);
const value = {p1: 'value', p2: 2, p3: true};
testee.put('key', value);
expect(testee.get('key')).toEqual(value);
});

it('should return `undefined` if no item is associated with the given key', () => {
const testee = new WebStorage(window.sessionStorage);
expect(testee.get('key')).toBeUndefined();
});

it('should return `null` if the `null` value is associated with the given key', () => {
const testee = new WebStorage(window.sessionStorage);
testee.put('key', null);
expect(testee.get('key')).toBeNull();
});

it('should return `undefined` if the `undefined` value is associated with the given key', () => {
const testee = new WebStorage(window.sessionStorage);
testee.put('key', undefined);
expect(testee.get('key')).toBeUndefined();
});

it('should return `0` if the `0` value is associated with the given key (falsy value)', () => {
const testee = new WebStorage(window.sessionStorage);
testee.put('key', 0);
expect(testee.get('key')).toEqual(0);
});

it('should return `false` if the `false` value is associated with the given key (falsy value)', () => {
const testee = new WebStorage(window.sessionStorage);
testee.put('key', false);
expect(testee.get('key')).toBeFalse();
});

it('should return an empty string if the "" value is associated with the given key (falsy value)', () => {
const testee = new WebStorage(window.sessionStorage);
testee.put('key', '');
expect(testee.get('key')).toEqual('');
});

it('should throw if failed to parse the value associated with the given key', () => {
const testee = new WebStorage(window.sessionStorage);
sessionStorage.setItem('key', 'VALUE NOT IN JSON FORMAT');
expect(() => testee.get('key')).toThrowError(/Unexpected token V in JSON/);
});
});

describe('WebStorage.isPresent', () => {

it('should not be present if no value is associated with the key', async () => {
it('should not be present if no value is associated with the key', () => {
const testee = new WebStorage(window.sessionStorage);
expect(testee.isPresent('key')).toBeFalse();
});

it('should be present if the `null` value is associated with the key', async () => {
it('should be present if the `null` value is associated with the key', () => {
const testee = new WebStorage(window.sessionStorage);
sessionStorage.setItem('key', null);
expect(testee.isPresent('key')).toBeTrue();
});

it('should be present if the `undefined` value is associated with the key', async () => {
it('should be present if the `undefined` value is associated with the key', () => {
const testee = new WebStorage(window.sessionStorage);
sessionStorage.setItem('key', undefined);
expect(testee.isPresent('key')).toBeTrue();
});

it('should be present if a value is associated with the key', async () => {
it('should be present if a value is associated with the key', () => {
const testee = new WebStorage(window.sessionStorage);
sessionStorage.setItem('key', 'value');
expect(testee.isPresent('key')).toBeTrue();
});

it('should be present if a falsy value is associated with the key', async () => {
it('should be present if a falsy value is associated with the key', () => {
const testee = new WebStorage(window.sessionStorage);
testee.put('key', 0);
expect(testee.isPresent('key')).toBeTrue();
Expand Down
66 changes: 39 additions & 27 deletions projects/scion/toolkit/storage/src/web-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

import {EMPTY, fromEvent, merge, Observable, Observer, of, Subject, TeardownLogic} from 'rxjs';
import {distinctUntilChanged, filter, mapTo, mergeMap, startWith, takeUntil} from 'rxjs/operators';
import {Defined} from '@scion/toolkit/util';

/**
* Allows observing items contained in web storage (local storage or session storage).
Expand Down Expand Up @@ -61,6 +60,32 @@ export class WebStorage {
}
}

/**
* Returns the item associated with the given key, or `undefined` if not found.
*
* Expects the value to be stored in JSON format. Throws an error if the value cannot be parsed.
*/
public get<T>(key: string): T | null | undefined {
const item = this._storage.getItem(key);

// Web storage returns `null` if there is no item associated with the key.
if (item === null) {
return undefined;
}

// If the value `undefined` is associated with the key, web storage returns 'undefined' as string.
if (item === 'undefined') {
return undefined;
}

// If the value `null` is associated with the key, web storage returns 'null' as string.
if (item === 'null') {
return null;
}

return JSON.parse(item);
}

/**
* Removes the item associated with the given key.
*/
Expand Down Expand Up @@ -88,6 +113,7 @@ export class WebStorage {
* with the given key upon subscription. By default, `emitIfAbsent` is set to `false`.
*/
public observe$<T>(key: string, options?: {emitIfAbsent?: boolean}): Observable<T> {
const emitIfAbsent = options?.emitIfAbsent ?? false;
const otherDocumentChange$ = fromEvent<StorageEvent>(window, 'storage')
.pipe(
filter(event => event.storageArea === this._storage),
Expand All @@ -102,32 +128,18 @@ export class WebStorage {
filter(itemKey => itemKey === key),
startWith(key),
mergeMap(itemKey => {
const item = this._storage.getItem(itemKey);

// Web storage returns `null` if there is no item associated with the key.
if (item === null) {
return Defined.orElse(options?.emitIfAbsent, false) ? of(undefined) : EMPTY;
}

// If the value `undefined` is associated with the key, web storage returns 'undefined' as string.
if (item === 'undefined') {
return of(undefined);
}

// If the value `null` is associated with the key, web storage returns 'null' as string.
if (item === 'null') {
return of(null);
}

try {
return of(JSON.parse(item));
}
catch (error) {
console.warn(`Failed to parse item from storage. Invalid JSON. [item=${item}]`, error);
return EMPTY;
}
},
),
if (!this.isPresent(itemKey)) {
return emitIfAbsent ? of(undefined) : EMPTY;
}

try {
return of(this.get<any>(itemKey));
}
catch (error) {
console.warn(`Failed to parse item from storage. Invalid JSON. [key=${itemKey}]`, error);
return EMPTY;
}
}),
distinctUntilChanged(),
takeUntil(unsubscribe$),
)
Expand Down

0 comments on commit 4965ed8

Please sign in to comment.