Added node.js directory storage
This commit is contained in:
parent
340043beb7
commit
b490f08862
|
@ -26,7 +26,7 @@ npm install tk-storage
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { getLocalStorage } from "tk-storage";
|
import { getLocalStorage } from "tk-storage";
|
||||||
const storage = getLocalStorage();
|
const storage = getLocalStorage("myapp");
|
||||||
```
|
```
|
||||||
|
|
||||||
Import in browser:
|
Import in browser:
|
||||||
|
@ -36,14 +36,14 @@ Import in browser:
|
||||||
```
|
```
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const storage = tkStorage.getLocalStorage();
|
const storage = tkStorage.getLocalStorage("myapp");
|
||||||
```
|
```
|
||||||
|
|
||||||
Use
|
Use
|
||||||
---
|
---
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const storage = getLocalStorage();
|
const storage = getLocalStorage("myapp");
|
||||||
await storage.get("key"); // => null
|
await storage.get("key"); // => null
|
||||||
await storage.set("key", "value");
|
await storage.set("key", "value");
|
||||||
await storage.get("key"); // => "value"
|
await storage.get("key"); // => "value"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# source activate_node
|
# source activate_node
|
||||||
|
|
||||||
vdir="./.venv"
|
vdir="./.venv"
|
||||||
expected="10.16.3"
|
expected="12.13.0"
|
||||||
|
|
||||||
if [ \! -f "./activate_node" ]
|
if [ \! -f "./activate_node" ]
|
||||||
then
|
then
|
||||||
|
|
|
@ -8,6 +8,10 @@ module.exports = {
|
||||||
"json",
|
"json",
|
||||||
"node"
|
"node"
|
||||||
],
|
],
|
||||||
|
watchPathIgnorePatterns: [
|
||||||
|
"<rootDir>/dist/",
|
||||||
|
"<rootDir>/node_modules/",
|
||||||
|
],
|
||||||
restoreMocks: true,
|
restoreMocks: true,
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
collectCoverageFrom: [
|
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",
|
"name": "tk-storage",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Javascript/Typescript persistent storage, with key-value stores as foundation",
|
"description": "Javascript/Typescript persistent storage, with key-value stores as foundation",
|
||||||
"main": "dist/tk-storage.umd.js",
|
"main": "dist/tk-storage.umd.js",
|
||||||
"source": "src/index.ts",
|
"source": "src/index.ts",
|
||||||
|
@ -14,11 +14,12 @@
|
||||||
"dev:test": "jest --watchAll",
|
"dev:test": "jest --watchAll",
|
||||||
"dev:build": "microbundle watch -f modern,umd",
|
"dev:build": "microbundle watch -f modern,umd",
|
||||||
"prepare": "npm run build",
|
"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": {
|
"devDependencies": {
|
||||||
"@types/uuid": "^3.4.5",
|
"@types/uuid": "^3.4.6",
|
||||||
"tk-base": "^0.2.1",
|
"tk-base": "^0.2.5",
|
||||||
"uuid": "^3.3.3"
|
"uuid": "^3.3.3"
|
||||||
},
|
},
|
||||||
"files": [
|
"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> {
|
export async function basicCheck(storage: KeyValueStorage): Promise<void> {
|
||||||
expect(await storage.get("test")).toBe(null);
|
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 () => {
|
it("uses a target storage, scoping its keys", async () => {
|
||||||
const reference1 = new MemoryStorage();
|
const reference1 = new MemoryStorage();
|
||||||
const reference2 = new MemoryStorage();
|
const reference2 = new MemoryStorage();
|
||||||
const target = new MemoryStorage();
|
const target = new MemoryStorage();
|
||||||
|
|
||||||
let storage = new ScopedStorage(reference1, () => target);
|
let storage = new RefScopedStorage(reference1, () => target);
|
||||||
await basicCheck(storage);
|
await basicCheck(storage);
|
||||||
|
|
||||||
await storage.set("persist", "42");
|
await storage.set("persist", "42");
|
||||||
expect(await storage.get("persist")).toBe("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");
|
expect(await storage.get("persist")).toBe("42");
|
||||||
|
|
||||||
storage = new ScopedStorage(reference2, target);
|
storage = new RefScopedStorage(reference2, target);
|
||||||
await storage.set("other", "thing");
|
await storage.set("other", "thing");
|
||||||
expect(await storage.get("persist")).toBe(null);
|
expect(await storage.get("persist")).toBe(null);
|
||||||
expect(await storage.get("other")).toBe("thing");
|
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("persist")).toBe("42");
|
||||||
expect(await storage.get("other")).toBe(null);
|
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)
|
* 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 reference: KeyValueStorage
|
||||||
private readonly target: KeyValueStorage
|
private readonly target: KeyValueStorage
|
||||||
private suffix?: string
|
private inner?: KeyValueStorage
|
||||||
|
|
||||||
constructor(reference: StorageDelegate, target: StorageDelegate) {
|
constructor(reference: StorageDelegate, target: StorageDelegate) {
|
||||||
this.reference = fromDelegate(reference);
|
this.reference = fromDelegate(reference);
|
||||||
this.target = fromDelegate(target);
|
this.target = fromDelegate(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async init(): Promise<void> {
|
private async init(): Promise<KeyValueStorage> {
|
||||||
const refkey = "tk-storage-scope-suffix"
|
const refkey = "tk-storage-scope-ref";
|
||||||
const suffix = await this.reference.get(refkey);
|
const suffix = await this.reference.get(refkey);
|
||||||
if (suffix) {
|
if (suffix) {
|
||||||
this.suffix = suffix;
|
return new ScopedStorage(this.target, suffix);
|
||||||
} else {
|
} else {
|
||||||
const suffix = "#" + uuid1();
|
const suffix = uuid1();
|
||||||
await this.reference.set(refkey, suffix);
|
await this.reference.set(refkey, suffix);
|
||||||
this.suffix = suffix;
|
return new ScopedStorage(this.target, suffix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(key: string): Promise<string | null> {
|
async get(key: string): Promise<string | null> {
|
||||||
if (!this.suffix) {
|
if (!this.inner) {
|
||||||
await this.init();
|
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> {
|
async set(key: string, value: string | null): Promise<void> {
|
||||||
if (!this.suffix) {
|
if (!this.inner) {
|
||||||
await this.init();
|
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";
|
import { KeyValueStorage } from "./basic";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key-value store
|
* Key-value store using localStorage
|
||||||
*/
|
*/
|
||||||
export class BrowserLocalStorage implements KeyValueStorage {
|
export class BrowserLocalStorage implements KeyValueStorage {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -1,24 +1,43 @@
|
||||||
import { getLocalStorage } from ".";
|
import { getLocalStorage } from ".";
|
||||||
import { MemoryStorage } from "./basic";
|
import { MemoryStorage, ScopedStorage } from "./basic";
|
||||||
import { BrowserLocalStorage } from "./browser";
|
import { BrowserLocalStorage } from "./browser";
|
||||||
|
import { NodeDirectoryStorage } from "./node";
|
||||||
|
import { forceNodeStoragesInTempDir } from "./node.test";
|
||||||
|
|
||||||
const localStorage = (window as any).localStorage;
|
const localStorage = (window as any).localStorage;
|
||||||
|
|
||||||
describe(getLocalStorage, () => {
|
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();
|
const mockWarn = jest.spyOn(console, "warn").mockImplementation();
|
||||||
|
|
||||||
let storage = getLocalStorage();
|
let storage = getLocalStorage("app1");
|
||||||
expect(storage).toBeInstanceOf(BrowserLocalStorage);
|
expect(storage).toBeInstanceOf(ScopedStorage);
|
||||||
|
expect((storage as any).target).toBeInstanceOf(BrowserLocalStorage);
|
||||||
expect(mockWarn).not.toHaveBeenCalled();
|
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"];
|
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(storage).toBeInstanceOf(MemoryStorage);
|
||||||
expect(mockWarn).toHaveBeenCalledWith("No persistent storage available, using in-memory volatile storage");
|
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 { BrowserLocalStorage } from "./browser";
|
||||||
|
import { NodeDirectoryStorage } from "./node";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère le meilleur stockage "local" disponible
|
* Récupère le meilleur stockage "local" disponible
|
||||||
*/
|
*/
|
||||||
export function getLocalStorage(): KeyValueStorage {
|
export function getLocalStorage(appname: string): KeyValueStorage {
|
||||||
if (typeof localStorage != "undefined") {
|
try {
|
||||||
return new BrowserLocalStorage();
|
return new ScopedStorage(new BrowserLocalStorage(), appname);
|
||||||
} else {
|
} catch {
|
||||||
console.warn("No persistent storage available, using in-memory volatile storage");
|
try {
|
||||||
return new MemoryStorage();
|
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",
|
"dom",
|
||||||
"es6"
|
"es6"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"src/**/*.test.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue