Added node.js directory storage
This commit is contained in:
parent
340043beb7
commit
b490f08862
13 changed files with 1207 additions and 458 deletions
|
@ -26,7 +26,7 @@ npm install tk-storage
|
|||
|
||||
```javascript
|
||||
import { getLocalStorage } from "tk-storage";
|
||||
const storage = getLocalStorage();
|
||||
const storage = getLocalStorage("myapp");
|
||||
```
|
||||
|
||||
Import in browser:
|
||||
|
@ -36,14 +36,14 @@ Import in browser:
|
|||
```
|
||||
|
||||
```javascript
|
||||
const storage = tkStorage.getLocalStorage();
|
||||
const storage = tkStorage.getLocalStorage("myapp");
|
||||
```
|
||||
|
||||
Use
|
||||
---
|
||||
|
||||
```javascript
|
||||
const storage = getLocalStorage();
|
||||
const storage = getLocalStorage("myapp");
|
||||
await storage.get("key"); // => null
|
||||
await storage.set("key", "value");
|
||||
await storage.get("key"); // => "value"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# source activate_node
|
||||
|
||||
vdir="./.venv"
|
||||
expected="10.16.3"
|
||||
expected="12.13.0"
|
||||
|
||||
if [ \! -f "./activate_node" ]
|
||||
then
|
||||
|
|
|
@ -8,6 +8,10 @@ module.exports = {
|
|||
"json",
|
||||
"node"
|
||||
],
|
||||
watchPathIgnorePatterns: [
|
||||
"<rootDir>/dist/",
|
||||
"<rootDir>/node_modules/",
|
||||
],
|
||||
restoreMocks: true,
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: [
|
||||
|
|
1408
package-lock.json
generated
1408
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tk-storage",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "Javascript/Typescript persistent storage, with key-value stores as foundation",
|
||||
"main": "dist/tk-storage.umd.js",
|
||||
"source": "src/index.ts",
|
||||
|
@ -14,11 +14,12 @@
|
|||
"dev:test": "jest --watchAll",
|
||||
"dev:build": "microbundle watch -f modern,umd",
|
||||
"prepare": "npm run build",
|
||||
"prepublishOnly": "npm test"
|
||||
"prepublishOnly": "npm test",
|
||||
"dev:serve": "live-server --host=localhost --port=5000 --no-browser --ignorePattern='.*\\.d\\.ts' dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^3.4.5",
|
||||
"tk-base": "^0.2.1",
|
||||
"@types/uuid": "^3.4.6",
|
||||
"tk-base": "^0.2.5",
|
||||
"uuid": "^3.3.3"
|
||||
},
|
||||
"files": [
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { KeyValueStorage, MemoryStorage, ScopedStorage } from "./basic";
|
||||
import { KeyValueStorage, MemoryStorage, RefScopedStorage } from "./basic";
|
||||
|
||||
export async function basicCheck(storage: KeyValueStorage): Promise<void> {
|
||||
expect(await storage.get("test")).toBe(null);
|
||||
|
@ -16,27 +16,27 @@ describe(MemoryStorage, () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe(ScopedStorage, () => {
|
||||
describe(RefScopedStorage, () => {
|
||||
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);
|
||||
let storage = new RefScopedStorage(reference1, () => target);
|
||||
await basicCheck(storage);
|
||||
|
||||
await storage.set("persist", "42");
|
||||
expect(await storage.get("persist")).toBe("42");
|
||||
|
||||
storage = new ScopedStorage(() => reference1, target);
|
||||
storage = new RefScopedStorage(() => reference1, target);
|
||||
expect(await storage.get("persist")).toBe("42");
|
||||
|
||||
storage = new ScopedStorage(reference2, target);
|
||||
storage = new RefScopedStorage(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);
|
||||
storage = new RefScopedStorage(reference1, target);
|
||||
expect(await storage.get("persist")).toBe("42");
|
||||
expect(await storage.get("other")).toBe(null);
|
||||
});
|
||||
|
|
47
src/basic.ts
47
src/basic.ts
|
@ -35,45 +35,64 @@ function fromDelegate(delegate: StorageDelegate): KeyValueStorage {
|
|||
}
|
||||
|
||||
/**
|
||||
* Use a target unscoped storage, scoping the keys in a virtual namespace
|
||||
* Wrap a storage, scoping the keys using a suffix
|
||||
*/
|
||||
export class ScopedStorage implements KeyValueStorage {
|
||||
private readonly target: KeyValueStorage
|
||||
|
||||
constructor(target: StorageDelegate, private readonly suffix: string) {
|
||||
this.target = fromDelegate(target);
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | null> {
|
||||
return await this.target.get(`${key}#${this.suffix}`);
|
||||
}
|
||||
|
||||
async set(key: string, value: string | null): Promise<void> {
|
||||
return await this.target.set(`${key}#${this.suffix}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a target unscoped storage, scoping the keys in a virtual random namespace
|
||||
*
|
||||
* The namespace is persisted in a reference storage (used unscoped)
|
||||
*/
|
||||
export class ScopedStorage implements KeyValueStorage {
|
||||
export class RefScopedStorage implements KeyValueStorage {
|
||||
private readonly reference: KeyValueStorage
|
||||
private readonly target: KeyValueStorage
|
||||
private suffix?: string
|
||||
private inner?: KeyValueStorage
|
||||
|
||||
constructor(reference: StorageDelegate, target: StorageDelegate) {
|
||||
this.reference = fromDelegate(reference);
|
||||
this.target = fromDelegate(target);
|
||||
}
|
||||
|
||||
private async init(): Promise<void> {
|
||||
const refkey = "tk-storage-scope-suffix"
|
||||
private async init(): Promise<KeyValueStorage> {
|
||||
const refkey = "tk-storage-scope-ref";
|
||||
const suffix = await this.reference.get(refkey);
|
||||
if (suffix) {
|
||||
this.suffix = suffix;
|
||||
return new ScopedStorage(this.target, suffix);
|
||||
} else {
|
||||
const suffix = "#" + uuid1();
|
||||
const suffix = uuid1();
|
||||
await this.reference.set(refkey, suffix);
|
||||
this.suffix = suffix;
|
||||
return new ScopedStorage(this.target, suffix);
|
||||
}
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | null> {
|
||||
if (!this.suffix) {
|
||||
await this.init();
|
||||
if (!this.inner) {
|
||||
this.inner = await this.init();
|
||||
}
|
||||
|
||||
return await this.target.get(key + this.suffix);
|
||||
return await this.inner.get(key);
|
||||
}
|
||||
|
||||
async set(key: string, value: string | null): Promise<void> {
|
||||
if (!this.suffix) {
|
||||
await this.init();
|
||||
if (!this.inner) {
|
||||
this.inner = await this.init();
|
||||
}
|
||||
|
||||
return await this.target.set(key + this.suffix, value);
|
||||
return await this.inner.set(key, value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { KeyValueStorage } from "./basic";
|
||||
|
||||
/**
|
||||
* Key-value store
|
||||
* Key-value store using localStorage
|
||||
*/
|
||||
export class BrowserLocalStorage implements KeyValueStorage {
|
||||
constructor() {
|
||||
|
|
|
@ -1,24 +1,43 @@
|
|||
import { getLocalStorage } from ".";
|
||||
import { MemoryStorage } from "./basic";
|
||||
import { MemoryStorage, ScopedStorage } from "./basic";
|
||||
import { BrowserLocalStorage } from "./browser";
|
||||
import { NodeDirectoryStorage } from "./node";
|
||||
import { forceNodeStoragesInTempDir } from "./node.test";
|
||||
|
||||
const localStorage = (window as any).localStorage;
|
||||
|
||||
describe(getLocalStorage, () => {
|
||||
it("returns the best storage available", () => {
|
||||
forceNodeStoragesInTempDir();
|
||||
|
||||
afterEach(() => {
|
||||
(window as any).localStorage = localStorage;
|
||||
});
|
||||
|
||||
it("returns the best storage available", async () => {
|
||||
const mockWarn = jest.spyOn(console, "warn").mockImplementation();
|
||||
|
||||
let storage = getLocalStorage();
|
||||
expect(storage).toBeInstanceOf(BrowserLocalStorage);
|
||||
let storage = getLocalStorage("app1");
|
||||
expect(storage).toBeInstanceOf(ScopedStorage);
|
||||
expect((storage as any).target).toBeInstanceOf(BrowserLocalStorage);
|
||||
expect(mockWarn).not.toHaveBeenCalled();
|
||||
|
||||
await storage.set("testkey", "testvalue");
|
||||
expect(await storage.get("testkey")).toBe("testvalue");
|
||||
|
||||
storage = getLocalStorage("app1");
|
||||
expect(await storage.get("testkey")).toBe("testvalue");
|
||||
|
||||
storage = getLocalStorage("app2");
|
||||
expect(await storage.get("testkey")).toBeNull();
|
||||
|
||||
delete (window as any)["localStorage"];
|
||||
storage = getLocalStorage();
|
||||
storage = getLocalStorage("app1");
|
||||
expect(storage).toBeInstanceOf(NodeDirectoryStorage);
|
||||
|
||||
(NodeDirectoryStorage.findUserData as any).mockImplementation(() => { throw new Error() });
|
||||
|
||||
storage = getLocalStorage("app1");
|
||||
expect(storage).toBeInstanceOf(MemoryStorage);
|
||||
expect(mockWarn).toHaveBeenCalledWith("No persistent storage available, using in-memory volatile storage");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(window as any).localStorage = localStorage;
|
||||
});
|
||||
|
|
19
src/index.ts
19
src/index.ts
|
@ -1,14 +1,19 @@
|
|||
import { KeyValueStorage, MemoryStorage } from "./basic";
|
||||
import { KeyValueStorage, MemoryStorage, ScopedStorage } from "./basic";
|
||||
import { BrowserLocalStorage } from "./browser";
|
||||
import { NodeDirectoryStorage } from "./node";
|
||||
|
||||
/**
|
||||
* 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();
|
||||
export function getLocalStorage(appname: string): KeyValueStorage {
|
||||
try {
|
||||
return new ScopedStorage(new BrowserLocalStorage(), appname);
|
||||
} catch {
|
||||
try {
|
||||
return new NodeDirectoryStorage(appname);
|
||||
} catch {
|
||||
console.warn("No persistent storage available, using in-memory volatile storage");
|
||||
return new MemoryStorage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
30
src/node.test.ts
Normal file
30
src/node.test.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { basicCheck } from "./basic.test";
|
||||
import { NodeDirectoryStorage } from "./node";
|
||||
|
||||
let tempdir: string | null = null
|
||||
|
||||
export function forceNodeStoragesInTempDir() {
|
||||
beforeEach(() => {
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const tmpdir = tempdir = fs.mkdtempSync(path.join(os.tmpdir(), "tk-storage-testing"));
|
||||
jest.spyOn(NodeDirectoryStorage, "findUserData").mockReturnValue(tmpdir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (tempdir) {
|
||||
// TODO remove directory
|
||||
tempdir = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe(NodeDirectoryStorage, () => {
|
||||
forceNodeStoragesInTempDir();
|
||||
|
||||
it("uses a directory as storage", async () => {
|
||||
const storage = new NodeDirectoryStorage("test-tk-storage");
|
||||
await basicCheck(storage);
|
||||
});
|
||||
});
|
82
src/node.ts
Normal file
82
src/node.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { KeyValueStorage } from "./basic";
|
||||
|
||||
/**
|
||||
* Key-value store, using local file-system
|
||||
*/
|
||||
export class NodeDirectoryStorage implements KeyValueStorage {
|
||||
readonly directory: string
|
||||
private checked = false
|
||||
|
||||
constructor(appname: string) {
|
||||
if (typeof require === "undefined") {
|
||||
throw new Error("Storage only available in nodejs");
|
||||
}
|
||||
const path = require("path");
|
||||
this.directory = path.join(NodeDirectoryStorage.findUserData(), "tk-storage", appname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the writable user data directory
|
||||
*/
|
||||
static findUserData(): string {
|
||||
const result = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share");
|
||||
if (!result) {
|
||||
throw new Error("Cannot locate user data directory");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file path for a key
|
||||
*/
|
||||
getPath(key: string): string {
|
||||
const path = require("path");
|
||||
return path.join(this.directory, key);
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | null> {
|
||||
const fs = require("fs");
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(this.getPath(key), { encoding: 'utf8' }, (err: any, data: string) => {
|
||||
resolve(err ? null : data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async set(key: string, value: string | null): Promise<void> {
|
||||
const fs = require("fs");
|
||||
|
||||
if (!this.checked) {
|
||||
await new Promise((resolve, reject) => {
|
||||
fs.mkdir(this.directory, { recursive: true }, (err: any) => {
|
||||
if (err && err.code !== 'EEXIST') {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
this.checked = true;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (value === null) {
|
||||
fs.unlink(this.getPath(key), (err: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
fs.writeFile(this.getPath(key), value, { encoding: 'utf8' }, (err: any) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -12,5 +12,10 @@
|
|||
"dom",
|
||||
"es6"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue