commit
340043beb7
15 changed files with 8408 additions and 0 deletions
@ -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 |
|||
========== |
|||
|
|||
[](https://gitlab.com/thunderk/tk-storage/commits/master) |
|||
[](https://gitlab.com/thunderk/tk-storage/commits/master) |
|||
[](https://npmjs.com/tk-storage) |
|||
[](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
@ -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