Skip to content

Commit

Permalink
feat: 🎸 v2 complete
Browse files Browse the repository at this point in the history
Tests completed with a mock into what would be react native's
AsyncStorage and community implementations of it (original package has
been deprecated). Works in real browser tests.

BREAKING CHANGE: 🧨 All functions return promises now. Options are different. Package
renamed from better-web-storage.
  • Loading branch information
damusix committed Apr 21, 2022
1 parent 62fd5d8 commit 2dbd0c6
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 18 deletions.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
{
"name": "bws2",
"name": "appstorage",
"version": "1.0.0",
"main": "cjs.js",
"module": "es.js",
"cdn": "browser.js",
"types": "types.d.ts",
"license": "MIT",
"private": true,
"author": {
"name": "Danilo Alonso",
"email": "[email protected]",
"url": "https://github.com/damusix"
},
"repository": "[email protected]:damusix/better-web-storage.git",
"scripts": {
"build": "sh scripts/build.sh",
"test": "mocha -r esm -r ./test/_setup.js -r ts-node/register 'test/**/*.ts'",
Expand Down
8 changes: 6 additions & 2 deletions scripts/updatePackage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
module,
cdn,
types,
license
license,
author,
repository
} from '../package.json';

const paths = {
Expand All @@ -26,7 +28,9 @@ writeFileSync(
module,
cdn,
types,
license
license,
author,
repository
})
);

Expand Down
56 changes: 46 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
class StorageError extends Error {};

interface StorageImplementation {
getItem(K: string): string | null | Promise<string | null>
setItem(K: string, V: string): void | Promise<void>
removeItem(key: string): void | Promise<void>;
clear(): void | Promise<void>;
length: number
}
type MaybePromise<T> = T | Promise<T>;

export type StorageImplementation = {
clear(): MaybePromise<void>;
getItem(key: string): MaybePromise<string | null>;
removeItem(key: string): MaybePromise<void>;
setItem(key: string, value: string): MaybePromise<void>;
multiGet?(keys: string[]): Promise<[string, string][]>;
length: number,
} & Record<string, string>

// interface StorageImplementation {
// getItem(K: string): string | null | Promise<string | null>
// setItem(K: string, V: string): void | Promise<void>
// removeItem(key: string): void | Promise<void>;
// clear(): void | Promise<void>;
// length: number
// }

type JsonStringifyable = (
null | number | string | boolean |
Expand Down Expand Up @@ -183,19 +194,44 @@ export class AppStorage {
* whether the keys exist within the storage or not
* @param keys
*/
has(keys: string[]): boolean[]
has(keys: string[]): Promise<boolean[]>

/**
* Returns whether key exists within the storage
* @param key
*/
has(key: string): boolean
has(key: string): Promise<boolean>


has(keyOrKeys: any): unknown {
async has(keyOrKeys: any): Promise<unknown> {

this._assertKey(keyOrKeys[0]);

// Handle async storage
if (!!this.storage.multiGet) {

let keys = [keyOrKeys];

if (Array.isArray(keyOrKeys)) {

keys = keyOrKeys;
}

const values = (

await this.storage.multiGet(keys)
).map(([key]) => key);

const found = keys.map(key => values.indexOf(key) !== -1);

if (found.length === 1) {

return found.pop();
}

return found;
}

if (Array.isArray(keyOrKeys)) {

return keyOrKeys.map(
Expand Down
271 changes: 268 additions & 3 deletions test/test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,273 @@
import { AppStorage, StorageImplementation } from '../build';
import { expect } from 'chai';

const clearStores = () => {

describe('SimpleAppStorage', function () {
window.localStorage.clear()
window.sessionStorage.clear()
}

it('should create an instance', function () {
const defineProps = (obj: any, props: Record<string, any>) => {

const entries = Object.entries(props);

Object.defineProperties(obj, entries.reduce((o, [key, val]) => {

return {
...o,
[key]: {
configurable: false,
enumerable: false,
value: val,

}
};
}, {}));
}

const fakeStorage = {
clear() {

for (const key of Object.keys(asyncStorage)) {

delete asyncStorage[key];
}

return Promise.resolve();
},
getItem(key: string) {

return Promise.resolve(asyncStorage[key])
},
removeItem(key: string) {

delete asyncStorage[key];

return Promise.resolve();
},
setItem: (key: string, value: string) => {

asyncStorage[key] = value;

return Promise.resolve();
},
multiGet: (keys: string[]) => {

return Object.entries(asyncStorage).filter(
([key]) => keys.includes(key)
);
}
};

const asyncStorage = {} as StorageImplementation;

defineProps(asyncStorage, fakeStorage)
Object.defineProperty(asyncStorage, 'length', {

configurable: false,
enumerable: false,
get() {

return Object.keys(asyncStorage).length;
}
})


describe('AppStorage', () => {

before(clearStores)
after(clearStores)

it('References localStorage or sessionStorage', () => {

const ls = new AppStorage(window.localStorage);
const ss = new AppStorage(window.sessionStorage);

expect(ls.storage).to.equal(window.localStorage);
expect(ss.storage).to.equal(window.sessionStorage);
});

// Test both local storage and session storage
const testSuites: [string, StorageImplementation][] = [
// name storage reference
['LocalStorage', window.localStorage],
['SessionStorage', window.sessionStorage],
['AsyncStorage', asyncStorage]
];

testSuites.forEach(([name, storage]) => {

const store: {
store: AppStorage;
prefixed?: AppStorage;
} = {
store: new AppStorage(storage)
};

it(`${name}: Sets json stringified keys`, async () => {


const val = [1,2,3,4];
await store.store.set('test', val);

expect(storage.test).to.equal(JSON.stringify(val));
});

it(`${name}: Sets an entire object as keys`, async () => {

const val = {
one: 'two',
buckle: 'myshow'
};

await store.store.set(val);

expect(storage.one).to.equal(JSON.stringify(val.one));
expect(storage.buckle).to.equal(JSON.stringify(val.buckle));
});

it(`${name}: Gets and parses a single key`, async () => {

expect(await store.store.get('test')).to.include.members([1,2,3,4])
});

it(`${name}: Gets and returns an object of key values when passed multiple values`, async () => {

const vals = await store.store.get(['one', 'buckle']);
expect(vals).to.include({
one: 'two',
buckle: 'myshow'
});
});

it(`${name}: Checks to see if has keys`, async () => {

expect(await store.store.has('test')).to.equal(true);
expect(await store.store.has(['test', 'one', 'buckle'])).to.have.members([true, true, true]);
expect(await store.store.has(['test', 'one', 'buckle', 'three'])).to.have.members([true, true, true, false]);
});

it(`${name}: Removes item from store`, async () => {

await store.store.rm('test');
expect(await store.store.get('test')).to.not.exist;
});

it(`${name}: Retrieves storage length`, async () => {

await store.store.set('test1', true);
await store.store.set('test2', true);
await store.store.set('test3', true);

expect(store.store.length).to.equal(5);
});

it(`${name}: Retrieves a copy of entire store`, async () => {

expect(await store.store.get()).to.include({
one: 'two',
buckle: 'myshow',
test1: true,
test2: true,
test3: true
});
});

it(`${name}: Clears storage`, async () => {

await store.store.clear();
expect(store.store.length).to.equal(0);
expect(storage.length).to.equal(0);
})

it(`${name}: Sets keys using a prefix`, async () => {

store.prefixed = new AppStorage(storage, 'test');
const store2 = new AppStorage(storage, 'test2');

await store.prefixed!.set('test', true);
await store2.set('test', false);

expect(storage.test).to.not.exist;
expect(storage['test:test']).to.exist;
expect(storage['test2:test']).to.exist;
expect(storage['test:test']).to.equal('true');
expect(storage['test2:test']).to.equal('false');
});

it(`${name}: Retrieves prefixed storage length accurately`, async () => {

await store.prefixed!.set('test1', true);
await store.prefixed!.set('test2', true);
await store.prefixed!.set('test3', true);
expect(await storage.length).to.equal(5);
expect(await store.prefixed!.length).to.equal(4);
});

it(`${name}: Retrieves all prefixed items`, async () => {

expect(await store.prefixed!.get()).to.include({
test: true,
test1: true,
test2: true,
test3: true
});
})

it(`${name}: Clears all prefixed items`, async () => {

await store.prefixed!.clear();
expect(store.prefixed!.length).to.equal(0);
expect(storage.length).to.equal(1);
});

it(`${name}: Object assigns to a current object in store`, async () => {

const cur = { a: true };
const assign = { b: false };
await store.store.set('cur', cur);

expect(await store.store.get('cur')).to.not.include(assign);

await store.store.assign('cur', assign);

expect(await store.store.get('cur')).to.include(cur);
expect(await store.store.get('cur')).to.include(assign);
});

it(`${name}: Does not allow assigns if value not an object`, async () => {

const cur = { a: true };
const assign = 'wat';
await store.store.set('cur', cur);

try {

await store.store.assign('cur', assign as any);
expect(await store.store.get('cur')).to.not.include(assign);
}
catch (e) {

expect(e).to.be.an('error');
}
});

it(`${name}: Does not allow assigns if item not an object`, async () => {

const cur = 'wat';
const assign = { a: true };
await store.store.set('cur', cur);

try {

await store.store.assign('cur', assign);
expect(await store.store.get('cur')).to.not.include(assign);
}
catch (e) {

expect(e).to.be.an('error');
}
});
});
});

});
Loading

0 comments on commit 2dbd0c6

Please sign in to comment.