Initial pre-version with only browser local storage
This commit is contained in:
commit
340043beb7
15 changed files with 8408 additions and 0 deletions
10
.editorconfig
Normal file
10
.editorconfig
Normal 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
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.venv
|
||||
node_modules
|
||||
.rts2_cache_*
|
||||
.coverage
|
||||
/dist/
|
11
.gitlab-ci.yml
Normal file
11
.gitlab-ci.yml
Normal 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
50
README.md
Normal 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
16
activate_node
Normal 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
23
jest.config.js
Normal 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
8029
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
46
package.json
Normal file
46
package.json
Normal 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
43
src/basic.test.ts
Normal 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
79
src/basic.ts
Normal 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
18
src/browser.test.ts
Normal 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
24
src/browser.ts
Normal 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
24
src/index.test.ts
Normal 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
14
src/index.ts
Normal 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
16
tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue