Initial pre-version with only browser local storage
This commit is contained in:
commit
340043beb7
|
@ -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
|
|
@ -0,0 +1,5 @@
|
||||||
|
.venv
|
||||||
|
node_modules
|
||||||
|
.rts2_cache_*
|
||||||
|
.coverage
|
||||||
|
/dist/
|
|
@ -0,0 +1,11 @@
|
||||||
|
image: node:latest
|
||||||
|
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- node_modules/
|
||||||
|
|
||||||
|
test:
|
||||||
|
before_script:
|
||||||
|
- npm install
|
||||||
|
script:
|
||||||
|
- npm test
|
|
@ -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"
|
||||||
|
```
|
|
@ -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"
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
});
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
});
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 New Issue