Initial pre-version with only browser local storage

This commit is contained in:
Michaël Lemaire 2019-10-02 22:12:55 +02:00
commit 340043beb7
15 changed files with 8408 additions and 0 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
max_line_length = off

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.venv
node_modules
.rts2_cache_*
.coverage
/dist/

11
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,11 @@
image: node:latest
cache:
paths:
- node_modules/
test:
before_script:
- npm install
script:
- npm test

50
README.md Normal file
View file

@ -0,0 +1,50 @@
tk-storage
==========
[![pipeline status](https://gitlab.com/thunderk/tk-storage/badges/master/pipeline.svg)](https://gitlab.com/thunderk/tk-storage/commits/master)
[![coverage report](https://gitlab.com/thunderk/tk-storage/badges/master/coverage.svg)](https://gitlab.com/thunderk/tk-storage/commits/master)
[![npm version](https://img.shields.io/npm/v/tk-storage.svg)](https://npmjs.com/tk-storage)
[![npm size](https://img.shields.io/bundlephobia/min/tk-storage.svg)](https://bundlephobia.com/result?p=tk-storage)
About
-----
Javascript/Typescript persistent storage, with key-value stores as foundation.
Typescript definitions are included.
Issues can be reported on [GitLab](https://gitlab.com/thunderk/tk-storage/issues).
Install
-------
Import in node:
```shell
npm install tk-storage
```
```javascript
import { getLocalStorage } from "tk-storage";
const storage = getLocalStorage();
```
Import in browser:
```html
<script src="https://unpkg.com/tk-storage"></script>
```
```javascript
const storage = tkStorage.getLocalStorage();
```
Use
---
```javascript
const storage = getLocalStorage();
await storage.get("key"); // => null
await storage.set("key", "value");
await storage.get("key"); // => "value"
```

16
activate_node Normal file
View file

@ -0,0 +1,16 @@
# Activation script for virtual nodejs environment
# Usage:
# source activate_node
vdir="./.venv"
expected="10.16.3"
if [ \! -f "./activate_node" ]
then
echo "Not in project directory"
exit 1
fi
test -x "${vdir}/bin/nodeenv" || ( python3 -m venv "${vdir}" && "${vdir}/bin/pip" install --upgrade nodeenv )
test "$(${vdir}/node/bin/nodejs --version)" = "v${expected}" || "${vdir}/bin/nodeenv" --node=${expected} --force "${vdir}/node"
source "${vdir}/node/bin/activate"

23
jest.config.js Normal file
View file

@ -0,0 +1,23 @@
module.exports = {
transform: {
"^.+\\.ts$": "ts-jest"
},
moduleFileExtensions: [
"ts",
"js",
"json",
"node"
],
restoreMocks: true,
collectCoverage: true,
collectCoverageFrom: [
"src/**/*.ts",
"!src/**/*.test.ts",
],
coverageDirectory: ".coverage",
coverageReporters: [
"lcovonly",
"html",
"text-summary"
]
}

8029
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

46
package.json Normal file
View file

@ -0,0 +1,46 @@
{
"name": "tk-storage",
"version": "0.1.0",
"description": "Javascript/Typescript persistent storage, with key-value stores as foundation",
"main": "dist/tk-storage.umd.js",
"source": "src/index.ts",
"module": "dist/tk-storage.modern.js",
"types": "dist/index.d.ts",
"scripts": {
"test": "jest",
"normalize": "tk-base",
"build": "microbundle build -f modern,umd",
"dev": "run-p dev:*",
"dev:test": "jest --watchAll",
"dev:build": "microbundle watch -f modern,umd",
"prepare": "npm run build",
"prepublishOnly": "npm test"
},
"devDependencies": {
"@types/uuid": "^3.4.5",
"tk-base": "^0.2.1",
"uuid": "^3.3.3"
},
"files": [
"/src",
"/dist"
],
"author": {
"name": "Michaël Lemaire",
"url": "https://thunderk.net"
},
"repository": {
"type": "git",
"url": "https://code.thunderk.net/tslib/tk-storage.git"
},
"bugs": {
"url": "https://gitlab.com/thunderk/tk-storage/issues"
},
"homepage": "https://code.thunderk.net/tslib/tk-storage",
"license": "ISC",
"keywords": [
"typescript",
"storage",
"database"
]
}

43
src/basic.test.ts Normal file
View file

@ -0,0 +1,43 @@
import { KeyValueStorage, MemoryStorage, ScopedStorage } from "./basic";
export async function basicCheck(storage: KeyValueStorage): Promise<void> {
expect(await storage.get("test")).toBe(null);
await storage.set("test", "val");
expect(await storage.get("test")).toBe("val");
expect(await storage.get("notest")).toBe(null);
await storage.set("test", null);
expect(await storage.get("test")).toBe(null);
}
describe(MemoryStorage, () => {
it("stores values in memory", async () => {
const storage = new MemoryStorage();
await basicCheck(storage);
});
});
describe(ScopedStorage, () => {
it("uses a target storage, scoping its keys", async () => {
const reference1 = new MemoryStorage();
const reference2 = new MemoryStorage();
const target = new MemoryStorage();
let storage = new ScopedStorage(reference1, () => target);
await basicCheck(storage);
await storage.set("persist", "42");
expect(await storage.get("persist")).toBe("42");
storage = new ScopedStorage(() => reference1, target);
expect(await storage.get("persist")).toBe("42");
storage = new ScopedStorage(reference2, target);
await storage.set("other", "thing");
expect(await storage.get("persist")).toBe(null);
expect(await storage.get("other")).toBe("thing");
storage = new ScopedStorage(reference1, target);
expect(await storage.get("persist")).toBe("42");
expect(await storage.get("other")).toBe(null);
});
});

79
src/basic.ts Normal file
View file

@ -0,0 +1,79 @@
import uuid1 from "uuid/v1";
/**
* Standard interface for the simplest key-value store
*/
export interface KeyValueStorage {
get(key: string): Promise<string | null>
set(key: string, value: string | null): Promise<void>
}
/**
* Basic memory implementation
*/
export class MemoryStorage implements KeyValueStorage {
private store: { [key: string]: string } = {}
async get(key: string): Promise<string | null> {
const result = this.store[key];
return (typeof result == "undefined") ? null : result;
}
async set(key: string, value: string | null): Promise<void> {
if (value === null) {
delete this.store[key];
} else {
this.store[key] = value;
}
}
}
type StorageDelegate = KeyValueStorage | (() => KeyValueStorage)
function fromDelegate(delegate: StorageDelegate): KeyValueStorage {
return (typeof delegate === "function") ? delegate() : delegate;
}
/**
* Use a target unscoped storage, scoping the keys in a virtual namespace
*
* The namespace is persisted in a reference storage (used unscoped)
*/
export class ScopedStorage implements KeyValueStorage {
private readonly reference: KeyValueStorage
private readonly target: KeyValueStorage
private suffix?: string
constructor(reference: StorageDelegate, target: StorageDelegate) {
this.reference = fromDelegate(reference);
this.target = fromDelegate(target);
}
private async init(): Promise<void> {
const refkey = "tk-storage-scope-suffix"
const suffix = await this.reference.get(refkey);
if (suffix) {
this.suffix = suffix;
} else {
const suffix = "#" + uuid1();
await this.reference.set(refkey, suffix);
this.suffix = suffix;
}
}
async get(key: string): Promise<string | null> {
if (!this.suffix) {
await this.init();
}
return await this.target.get(key + this.suffix);
}
async set(key: string, value: string | null): Promise<void> {
if (!this.suffix) {
await this.init();
}
return await this.target.set(key + this.suffix, value);
}
}

18
src/browser.test.ts Normal file
View file

@ -0,0 +1,18 @@
import { basicCheck } from "./basic.test";
import { BrowserLocalStorage } from "./browser";
const localStorage = (window as any).localStorage;
describe(BrowserLocalStorage, () => {
it("uses localStorage as storage", async () => {
const storage = new BrowserLocalStorage();
await basicCheck(storage);
delete (window as any)["localStorage"];
expect(() => new BrowserLocalStorage()).toThrowError("localStorage not available");
});
});
afterEach(() => {
(window as any).localStorage = localStorage;
});

24
src/browser.ts Normal file
View file

@ -0,0 +1,24 @@
import { KeyValueStorage } from "./basic";
/**
* Key-value store
*/
export class BrowserLocalStorage implements KeyValueStorage {
constructor() {
if (typeof localStorage == "undefined" || !localStorage) {
throw new Error("localStorage not available");
}
}
async get(key: string): Promise<string | null> {
return localStorage.getItem(key);
}
async set(key: string, value: string | null): Promise<void> {
if (value === null) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, value);
}
}
}

24
src/index.test.ts Normal file
View file

@ -0,0 +1,24 @@
import { getLocalStorage } from ".";
import { MemoryStorage } from "./basic";
import { BrowserLocalStorage } from "./browser";
const localStorage = (window as any).localStorage;
describe(getLocalStorage, () => {
it("returns the best storage available", () => {
const mockWarn = jest.spyOn(console, "warn").mockImplementation();
let storage = getLocalStorage();
expect(storage).toBeInstanceOf(BrowserLocalStorage);
expect(mockWarn).not.toHaveBeenCalled();
delete (window as any)["localStorage"];
storage = getLocalStorage();
expect(storage).toBeInstanceOf(MemoryStorage);
expect(mockWarn).toHaveBeenCalledWith("No persistent storage available, using in-memory volatile storage");
});
});
afterEach(() => {
(window as any).localStorage = localStorage;
});

14
src/index.ts Normal file
View file

@ -0,0 +1,14 @@
import { KeyValueStorage, MemoryStorage } from "./basic";
import { BrowserLocalStorage } from "./browser";
/**
* Récupère le meilleur stockage "local" disponible
*/
export function getLocalStorage(): KeyValueStorage {
if (typeof localStorage != "undefined") {
return new BrowserLocalStorage();
} else {
console.warn("No persistent storage available, using in-memory volatile storage");
return new MemoryStorage();
}
}

16
tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"preserveConstEnums": true,
"declaration": true,
"target": "es6",
"lib": [
"dom",
"es6"
]
}
}