Skip to content

feat(core): Introduce new API #1010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/core/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
6 changes: 5 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"prepack": "yarn build",
"build": "bob build",
"test:lint": "eslint src/**",
"test:ts": "tsc --noEmit"
"test:ts": "tsc --noEmit",
"test:jest": "jest"
},
"keywords": [
"react-native",
Expand All @@ -30,8 +31,11 @@
"author": "Krzysztof Borowy <contact@kborowy.com>",
"license": "MIT",
"devDependencies": {
"@types/jest": "29.5.4",
"eslint": "8.26.0",
"jest": "29.5.0",
"react-native-builder-bob": "0.20.0",
"ts-jest": "^29.1.1",
"typescript": "4.9.5"
},
"react-native-builder-bob": {
Expand Down
71 changes: 71 additions & 0 deletions packages/core/src/Storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { StorageKeys, StorageModel, StorageEntry } from "./types/StorageModel";
import { AsyncStorage } from "./types/AsyncStorage";
import { StorageBackend } from "./types/StorageBackend";
import { StorageExtension } from "./types/Extension";
import { StorageConfig } from "./types/StorageConfig";
import { StorageAdapter } from "./types/StorageAdapter";

class Storage<S extends StorageModel, E extends StorageExtension | undefined>
implements AsyncStorage<S, E>
{
private adapter: StorageAdapter<S>;

constructor(private backend: StorageBackend<E>, config: StorageConfig<S>) {
this.adapter = config.adapter;
this.ext = this.backend.extension;
}

public ext: E;

getItem = async <K extends StorageKeys<S>>(key: K): Promise<S[K] | null> => {
const result = await this.backend.multiGet([key as string]);
return this.adapter.deserialize(key, result[key]);
};

setItem = async <K extends StorageKeys<S>>(
key: K,
value: S[K]
): Promise<void> => {
await this.backend.multiSet({ [key]: this.adapter.serialize(key, value) });
};

removeItem = async <K extends StorageKeys<S>>(key: K): Promise<void> => {
await this.backend.multiRemove([key as string]);
};

getMany = async <K extends StorageKeys<S>>(
keys: K[]
): Promise<{ [k in K]: S[k] | null }> => {
const result = await this.backend.multiGet(keys as string[]);
return keys.reduce((entries, key) => {
return {
...entries,
[key]: this.adapter.deserialize(key, result[key]),
};
}, {} as { [k in K]: S[k] | null });
};

setMany = async <K extends StorageKeys<S>>(entries: {
[k in K]: S[k] | null;
}): Promise<void> => {
const serialized = Object.entries(entries).reduce<StorageEntry>(
(prev, curr) => {
const [key, value] = curr;
return {
...prev,
[key]: this.adapter.serialize(key as K, value as S[K]),
};
},
{}
);
await this.backend.multiSet(serialized);
};

removeMany = async <K extends StorageKeys<S>>(keys: K[]): Promise<void> => {
await this.backend.multiRemove(keys as string[]);
};

clear = async (): Promise<void> => this.backend.drop();
}

export default Storage;
17 changes: 17 additions & 0 deletions packages/core/src/default-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { DefaultStorageModel, StorageKeys } from "./types/StorageModel";
import { StorageAdapter } from "./types/StorageAdapter";

export const DefaultAdapter: StorageAdapter<DefaultStorageModel> = {
deserialize<K extends StorageKeys<DefaultStorageModel>>(
_: K,
value: string | null
): DefaultStorageModel[K] | null {
return value;
},
serialize<K extends StorageKeys<DefaultStorageModel>>(
_: K,
value: DefaultStorageModel[K] | null
): string | null {
return value;
},
};
35 changes: 32 additions & 3 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
function double(i: number) {
return i * i;
import { StorageBackend } from "./types/StorageBackend";
import { StorageConfig } from "./types/StorageConfig";
import {
DefaultStorageModel,
StorageModel,
StorageEntry,
} from "./types/StorageModel";
import { AsyncStorage } from "./types/AsyncStorage";
import Storage from "./Storage";
import { StorageExtension } from "./types/Extension";
import { DefaultAdapter } from "./default-adapter";

export {
AsyncStorage,
StorageBackend,
StorageEntry,
StorageConfig,
StorageModel,
StorageExtension,
};

class AsyncStorageFactory {
private constructor() {}

static create<E extends StorageExtension | undefined>(
storage: StorageBackend<E>
): AsyncStorage<DefaultStorageModel, E> {
return new Storage<DefaultStorageModel, E>(storage, {
adapter: DefaultAdapter,
});
}
}

double(6);
export default AsyncStorageFactory;
25 changes: 25 additions & 0 deletions packages/core/src/types/AsyncStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { StorageKeys, StorageModel } from "./StorageModel";
import { StorageExtension } from "./Extension";

export interface AsyncStorage<
S extends StorageModel,
E extends StorageExtension | undefined
> {
getItem<K extends StorageKeys<S>>(key: K): Promise<S[K] | null>;
setItem<K extends StorageKeys<S>>(key: K, value: S[K]): Promise<void>;
removeItem<K extends StorageKeys<S>>(key: K): Promise<void>;

getMany<K extends StorageKeys<S>>(
keys: K[]
): Promise<{ [k in K]: S[k] | null }>;

setMany<K extends StorageKeys<S>>(entries: {
[k in K]: S[k] | null;
}): Promise<void>;

removeMany<K extends StorageKeys<S>>(keys: K[]): Promise<void>;

clear(): Promise<void>;

ext: E;
}
1 change: 1 addition & 0 deletions packages/core/src/types/Extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type StorageExtension = Record<string, unknown>;
13 changes: 13 additions & 0 deletions packages/core/src/types/StorageAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { StorageKeys, StorageModel } from "./StorageModel";

export interface StorageAdapter<S extends StorageModel> {
serialize<K extends StorageKeys<S>>(
key: K,
value: S[K] | null
): string | null;

deserialize<K extends StorageKeys<S>>(
key: K,
value: string | null
): S[K] | null;
}
10 changes: 10 additions & 0 deletions packages/core/src/types/StorageBackend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { StorageExtension } from "./Extension";
import { StorageEntry } from "./StorageModel";

export interface StorageBackend<E extends StorageExtension | undefined> {
multiGet(keys: readonly string[]): Promise<StorageEntry>;
multiSet(entries: StorageEntry): Promise<StorageEntry>;
multiRemove(keys: readonly string[]): Promise<void>;
drop(): Promise<void>;
extension: E;
}
6 changes: 6 additions & 0 deletions packages/core/src/types/StorageConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { StorageModel } from "./StorageModel";
import { StorageAdapter } from "./StorageAdapter";

export type StorageConfig<S extends StorageModel> = {
adapter: StorageAdapter<S>;
};
9 changes: 9 additions & 0 deletions packages/core/src/types/StorageModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type StorageModel<T = unknown> = {
[K in keyof T]: T[K];
};

export type StorageKeys<T> = keyof T;

export type StorageEntry = Record<string, string | null>;

export type DefaultStorageModel = StorageModel<{ [key in string]: string }>;
46 changes: 46 additions & 0 deletions packages/core/tests/TestStorage.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { StorageBackend, StorageEntry, StorageExtension } from "../src";

interface TestExtension extends StorageExtension {
double: (value: number) => number;
}

class TestStorageBackend implements StorageBackend<TestExtension> {
private storage = new Map<string, string | null>();

clearMock = () => {
this.storage.clear();
jest.clearAllMocks();
};

extension: TestExtension = {
double: jest.fn((value: number) => value * 2),
};

drop = jest.fn(async (): Promise<void> => {
this.storage.clear();
});

multiGet = jest.fn(async (keys: readonly string[]): Promise<StorageEntry> => {
return keys.reduce((entries, key) => {
return {
...entries,
[key]: this.storage.get(key) ?? null,
};
}, {} as StorageEntry);
});

multiRemove = jest.fn(async (keys: readonly string[]): Promise<void> => {
keys.forEach((key) => {
this.storage.delete(key);
});
});

multiSet = jest.fn(async (entries: StorageEntry): Promise<StorageEntry> => {
Object.entries(entries).forEach(([key, value]) => {
this.storage.set(key, value);
});
return entries;
});
}

export default TestStorageBackend;
33 changes: 33 additions & 0 deletions packages/core/tests/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import AsyncStorageFactory from "../src";
import TestStorageBackend from "./TestStorage.fixture";

describe("Async Storage", () => {
describe("Storage Backend usage", () => {
const testBackend = new TestStorageBackend();

beforeEach(() => {
testBackend.clearMock();
});

it("calls provided storage backend", async () => {
const storage = AsyncStorageFactory.create(testBackend);
const key = "test_key";
const value = "testing_value";
await storage.setItem(key, value);
let read = await storage.getItem(key);
expect(read).toEqual(value);
await storage.removeItem(key);
read = await storage.getItem(key);
expect(read).toEqual(null);
expect(storage.ext.double(2)).toEqual(4);

expect(testBackend.multiGet).toBeCalledTimes(2);
expect(testBackend.multiGet).toHaveBeenCalledWith([key]);
expect(testBackend.multiSet).toBeCalledTimes(1);
expect(testBackend.multiSet).toBeCalledWith({ [key]: value });
expect(testBackend.multiRemove).toBeCalledTimes(1);
expect(testBackend.multiRemove).toBeCalledWith([key]);
expect(testBackend.extension.double).toBeCalledTimes(1);
});
});
});
5 changes: 4 additions & 1 deletion packages/core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "../../.config/tsconfig.base.json",
"include": ["./src/**/*"]
"include": ["./src/**/*"],
"compilerOptions": {
"types": ["jest"]
}
}
Loading