Browse Source

Port to deno

master 1.0.0
Michaël Lemaire 6 months ago
parent
commit
33bc8f1863
  1. 2
      .dockerignore
  2. 9
      .editorconfig
  3. 8
      .gitignore
  4. 11
      .gitlab-ci.yml
  5. 8
      .vscode/settings.json
  6. 11
      Dockerfile
  7. 68
      README.md
  8. 16
      activate_node
  9. 12
      basic.test.ts
  10. 20
      basic.ts
  11. 4
      deps.ts
  12. 11
      dist/index.html
  13. 27
      jest.config.js
  14. 21
      local.test.ts
  15. 4
      local.ts
  16. 49
      mod.test.ts
  17. 63
      mod.ts
  18. 8943
      package-lock.json
  19. 60
      package.json
  20. 11
      remote.test.ts
  21. 66
      remote.ts
  22. 71
      server.ts
  23. 18
      src/browser.test.ts
  24. 51
      src/index.ts
  25. 30
      src/node.test.ts
  26. 82
      src/node.ts
  27. 33
      src/remote.test.ts
  28. 64
      src/remote.ts
  29. 64
      src/server.ts
  30. 57
      testing.ts
  31. 19
      tsconfig.json

2
.dockerignore

@ -1,2 +0,0 @@
node_modules
.venv

9
.editorconfig

@ -1,10 +1,9 @@
root = true
[*]
[*.{ts,json}]
charset = utf-8
end_of_line = lf
insert_final_newline = 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

8
.gitignore

@ -1,5 +1,3 @@
.venv
node_modules
.rts2_cache_*
.coverage
/dist/
deno.d.ts
.vscode
.local

11
.gitlab-ci.yml

@ -1,11 +0,0 @@
image: node:latest
cache:
paths:
- node_modules/
test:
before_script:
- npm install
script:
- npm test

8
.vscode/settings.json

@ -1,3 +1,7 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
"typescript.tsdk": "node_modules/typescript/lib",
"deno.enable": true,
"deno.suggest.imports.hosts": {
"https://deno.land": false
}
}

11
Dockerfile

@ -1,11 +0,0 @@
FROM alpine
RUN apk add --no-cache nodejs npm
ADD . /app
WORKDIR /app
RUN npm install
EXPOSE 5001
CMD npm run storageserver

68
README.md

@ -1,67 +1,3 @@
tk-storage
==========
# typescript/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("myapp");
```
Import in browser:
```html
<script src="https://unpkg.com/tk-storage"></script>
```
```javascript
const storage = tkStorage.getLocalStorage("myapp");
```
Use
---
To get a storage locally persistent (saved in browser data or on disk for Node.js):
```javascript
const storage = getLocalStorage("myapp");
await storage.get("key"); // => null
await storage.set("key", "value");
await storage.get("key"); // => "value"
```
To get a storage remotely persistent (saved on a compliant server):
```javascript
const storage = getRemoteStorage("myapp", "https://tk-storage.example.io/", { shared: true });
await storage.get("key"); // => null
await storage.set("key", "value");
await storage.get("key"); // => "value"
```
Run a server for remote storage:
```shell
npm run storageserver
```
[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/storage?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=storage)

16
activate_node

@ -1,16 +0,0 @@
# Activation script for virtual nodejs environment
# Usage:
# source activate_node
vdir="./.venv"
expected="12.13.0"
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"

12
src/basic.test.ts → basic.test.ts

@ -1,13 +1,5 @@
import { KeyValueStorage, MemoryStorage, RefScopedStorage } 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);
}
import { basicCheck, describe, expect, it } from "./testing.ts";
import { MemoryStorage, RefScopedStorage } from "./basic.ts";
describe(MemoryStorage, () => {
it("stores values in memory", async () => {

20
src/basic.ts → basic.ts

@ -1,18 +1,18 @@
import uuid1 from "uuid/v1";
import { uuid1 } from "./deps.ts";
/**
* 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>
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 } = {}
private store: { [key: string]: string } = {};
async get(key: string): Promise<string | null> {
const result = this.store[key];
@ -28,7 +28,7 @@ export class MemoryStorage implements KeyValueStorage {
}
}
type StorageDelegate = KeyValueStorage | (() => KeyValueStorage)
type StorageDelegate = KeyValueStorage | (() => KeyValueStorage);
function fromDelegate(delegate: StorageDelegate): KeyValueStorage {
return (typeof delegate === "function") ? delegate() : delegate;
@ -38,7 +38,7 @@ function fromDelegate(delegate: StorageDelegate): KeyValueStorage {
* Wrap a storage, scoping the keys using a suffix
*/
export class ScopedStorage implements KeyValueStorage {
private readonly target: KeyValueStorage
private readonly target: KeyValueStorage;
constructor(target: StorageDelegate, private readonly suffix: string) {
this.target = fromDelegate(target);
@ -59,9 +59,9 @@ export class ScopedStorage implements KeyValueStorage {
* The namespace is persisted in a reference storage (used unscoped)
*/
export class RefScopedStorage implements KeyValueStorage {
private readonly reference: KeyValueStorage
private readonly target: KeyValueStorage
private inner?: KeyValueStorage
private readonly reference: KeyValueStorage;
private readonly target: KeyValueStorage;
private inner?: KeyValueStorage;
constructor(reference: StorageDelegate, target: StorageDelegate) {
this.reference = fromDelegate(reference);
@ -74,7 +74,7 @@ export class RefScopedStorage implements KeyValueStorage {
if (suffix) {
return new ScopedStorage(this.target, suffix);
} else {
const suffix = uuid1();
const suffix = uuid1.generate().toString();
await this.reference.set(refkey, suffix);
return new ScopedStorage(this.target, suffix);
}

4
deps.ts

@ -0,0 +1,4 @@
export { v1 as uuid1 } from "https://deno.land/std@0.99.0/uuid/mod.ts";
export { json, opine } from "https://deno.land/x/opine@1.5.3/mod.ts";
export { opineCors } from "https://deno.land/x/cors@v1.2.2/mod.ts";
export { readAll } from "https://deno.land/std@0.99.0/io/util.ts";

11
dist/index.html

@ -1,11 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<script src="tk-storage.umd.js"></script>
</head>
<body>
</body>
</html>

27
jest.config.js

@ -1,27 +0,0 @@
module.exports = {
transform: {
"^.+\\.ts$": "ts-jest"
},
moduleFileExtensions: [
"ts",
"js",
"json",
"node"
],
watchPathIgnorePatterns: [
"<rootDir>/dist/",
"<rootDir>/node_modules/",
],
restoreMocks: true,
collectCoverage: true,
collectCoverageFrom: [
"src/**/*.ts",
"!src/**/*.test.ts",
],
coverageDirectory: ".coverage",
coverageReporters: [
"lcovonly",
"html",
"text-summary"
]
}

21
local.test.ts

@ -0,0 +1,21 @@
import {
basicCheck,
describe,
disableLocalStorage,
expect,
it,
} from "./testing.ts";
import { LocalStorage } from "./local.ts";
describe(LocalStorage, () => {
it("uses localStorage as storage", async () => {
const storage = new LocalStorage();
await basicCheck(storage);
await disableLocalStorage(async () => {
expect(() => new LocalStorage()).toThrow(
"localStorage not available",
);
});
});
});

4
src/browser.ts → local.ts

@ -1,9 +1,9 @@
import { KeyValueStorage } from "./basic";
import { KeyValueStorage } from "./basic.ts";
/**
* Key-value store using localStorage
*/
export class BrowserLocalStorage implements KeyValueStorage {
export class LocalStorage implements KeyValueStorage {
constructor() {
if (typeof localStorage == "undefined" || !localStorage) {
throw new Error("localStorage not available");

49
src/index.test.ts → mod.test.ts

@ -1,19 +1,8 @@
import { getLocalStorage, getMemoryStorage, getRemoteStorage } from ".";
import { MemoryStorage, ScopedStorage } from "./basic";
import { BrowserLocalStorage } from "./browser";
import { NodeDirectoryStorage } from "./node";
import { forceNodeStoragesInTempDir } from "./node.test";
import { startTestServer } from "./remote.test";
const localStorage = (window as any).localStorage;
describe(getLocalStorage, () => {
forceNodeStoragesInTempDir();
afterEach(() => {
(window as any).localStorage = localStorage;
});
import { getLocalStorage, getMemoryStorage, getRemoteStorage } from "./mod.ts";
import { MemoryStorage, ScopedStorage } from "./basic.ts";
import { describe, expect, it, runTestServer } from "./testing.ts";
/*describe(getLocalStorage, () => {
it("returns the best storage available", async () => {
const mockWarn = jest.spyOn(console, "warn").mockImplementation();
@ -35,32 +24,36 @@ describe(getLocalStorage, () => {
storage = getLocalStorage("app1");
expect(storage).toBeInstanceOf(NodeDirectoryStorage);
(NodeDirectoryStorage.findUserData as any).mockImplementation(() => { throw new Error() });
(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");
expect(mockWarn).toHaveBeenCalledWith(
"No persistent storage available, using in-memory volatile storage",
);
});
});
});*/
describe(getRemoteStorage, () => {
const server = startTestServer();
it("returns a scoped storage", async () => {
let storage = getRemoteStorage("app1", `http://localhost:${server.port()}`);
await runTestServer(async (url) => {
let storage = getRemoteStorage("app1", url);
await storage.set("testkey", "testvalue");
expect(await storage.get("testkey")).toBe("testvalue");
await storage.set("testkey", "testvalue");
expect(await storage.get("testkey")).toBe("testvalue");
storage = getRemoteStorage("app1", `http://localhost:${server.port()}`);
expect(await storage.get("testkey")).toBe("testvalue");
storage = getRemoteStorage("app1", url);
expect(await storage.get("testkey")).toBe("testvalue");
storage = getRemoteStorage("app2", `http://localhost:${server.port()}`);
expect(await storage.get("testkey")).toBeNull();
storage = getRemoteStorage("app2", url);
expect(await storage.get("testkey")).toBeNull();
});
});
it("scopes in a private namespace on shared mode", async () => {
// TODO
});
});

63
mod.ts

@ -0,0 +1,63 @@
import {
KeyValueStorage,
MemoryStorage,
RefScopedStorage,
ScopedStorage,
} from "./basic.ts";
import { LocalStorage } from "./local.ts";
import { RestRemoteStorage } from "./remote.ts";
/**
* Base type for storage usage
*/
export type TKStorage = KeyValueStorage;
/**
* Exposed classes
*/
export {
LocalStorage,
MemoryStorage,
RefScopedStorage,
RestRemoteStorage,
ScopedStorage,
};
/**
* Get the best "local" storage available
*/
export function getLocalStorage(appname: string): TKStorage {
try {
return new ScopedStorage(new LocalStorage(), appname);
} catch {
console.warn(
"No persistent storage available, using in-memory volatile storage",
);
return new MemoryStorage();
}
}
/**
* Get a remote storage, based on an URI
*
* If *shared* is set to true, a namespace will be used to avoid collisions, using getLocalStorage to persist it
*/
export function getRemoteStorage(
appname: string,
uri: string,
options = { shared: false },
): TKStorage {
let storage: TKStorage = new RestRemoteStorage(appname, uri);
storage = new ScopedStorage(storage, appname);
if (options.shared) {
storage = new RefScopedStorage(getLocalStorage(appname), storage);
}
return storage;
}
/**
* Get a in-memory volatile storage
*/
export function getMemoryStorage(): TKStorage {
return new MemoryStorage();
}

8943
package-lock.json

File diff suppressed because it is too large

60
package.json

@ -1,60 +0,0 @@
{
"name": "tk-storage",
"version": "0.3.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",
"storageserver": "ts-node src/server.ts",
"docker:build": "docker build -t thunderk/tk-storage .",
"docker:push": "docker push thunderk/tk-storage",
"docker:run": "docker run -it --rm thunderk/tk-storage",
"docker:publish": "run-s docker:build docker:push",
"prepare": "npm run build",
"prepublishOnly": "npm test",
"dev:serve": "live-server --host=localhost --port=5000 --no-browser --ignorePattern='.*\\.d\\.ts' dist"
},
"dependencies": {
"xmlhttprequest": "^1.8.0"
},
"devDependencies": {
"@types/cors": "^2.8.6",
"@types/express": "^4.17.2",
"@types/uuid": "^3.4.6",
"cors": "^2.8.5",
"express": "^4.17.1",
"get-port": "^5.0.0",
"tk-base": "^0.2.5",
"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"
]
}

11
remote.test.ts

@ -0,0 +1,11 @@
import { basicCheck, describe, it, runTestServer } from "./testing.ts";
import { RestRemoteStorage } from "./remote.ts";
describe(RestRemoteStorage, () => {
it("communicates with the server", async () => {
await runTestServer(async (url) => {
const storage = new RestRemoteStorage("test", url);
await basicCheck(storage);
});
});
});

66
remote.ts

@ -0,0 +1,66 @@
import { KeyValueStorage } from "./basic.ts";
function protectKey(key: string): string {
return encodeURIComponent(key);
}
type RestClient = (
method: "GET" | "POST" | "PUT" | "DELETE",
uri: string,
body?: string,
) => Promise<string | null>;
export const HEADER_REQUESTER = "X-TK-Storage-App";
export const HEADER_REPLYIER = "X-TK-Storage-Control";
/**
* Storage on a remote server, directly usable using HTTP REST verbs
*/
export class RestRemoteStorage implements KeyValueStorage {
readonly appname: string;
readonly base_url: string;
readonly client: RestClient;
constructor(appname: string, url: string) {
this.appname = appname;
this.base_url = (url[url.length - 1] != "/") ? url + "/" : url;
this.client = this.createClient();
}
async get(key: string): Promise<string | null> {
key = protectKey(key);
return await this.client("GET", key);
}
async set(key: string, value: string | null): Promise<void> {
key = protectKey(key);
if (value === null) {
await this.client("DELETE", key);
} else {
await this.client("PUT", key, value);
}
}
private createClient(): RestClient {
const client: RestClient = async (method, path, body?) => {
const response = await fetch(`${this.base_url}${path}`, {
method,
body,
headers: {
[HEADER_REQUESTER]: this.appname,
},
});
if (response.headers.get(HEADER_REPLYIER) != "ok") {
throw new Error("storage not compatible with tk-storage");
} else if (response.status == 200) {
return await response.text();
} else if (response.status == 404) {
return null;
} else {
throw new Error(
`http error ${response.status}: ${response.statusText}`,
);
}
};
return client;
}
}

71
server.ts

@ -0,0 +1,71 @@
#!/usr/bin/env -S deno run --allow-read --allow-net
import { getLocalStorage } from "./mod.ts";
import { KeyValueStorage } from "./basic.ts";
import { HEADER_REPLYIER, HEADER_REQUESTER } from "./remote.ts";
import { json, opine, opineCors, readAll } from "./deps.ts";
const PORT = 5001;
type Server = {
close: () => Promise<void>;
};
/**
* Start a server compliant with RestRemoteStorage
*/
export function startRestServer(
port: number,
storage: KeyValueStorage,
): Promise<Server> {
const app = opine();
app.use(
opineCors({ exposedHeaders: [HEADER_REPLYIER] }),
json(),
(req, res, next) => {
if (req.get(HEADER_REQUESTER)) {
res.set(HEADER_REPLYIER, "ok");
next();
} else {
res.setStatus(400).send();
}
},
);
app.get("*", (req, res, next) => {
storage.get(req.path).then((result) => {
if (result === null) {
res.setStatus(404).send();
} else {
res.send(result);
}
}).catch(next);
});
app.put("*", (req, res, next) => {
readAll(req.body).then((body) => {
const value = new TextDecoder().decode(body);
storage.set(req.path, value).then(() => {
res.send();
}).catch(next);
}).catch(next);
});
app.delete("*", (req, res, next) => {
storage.set(req.path, null).then(() => {
res.send();
}).catch(next);
});
return new Promise((resolve) => {
const server = app.listen(port, () => {
resolve({
close: () =>
new Promise((resolve, reject) => {
server.close();
}),
});
});
});
}
if (import.meta.main) {
await startRestServer(PORT, getLocalStorage("tk-storage-server"));
console.log(`Server running, use http://localhost:${PORT} to connect`);
}

18
src/browser.test.ts

@ -1,18 +0,0 @@
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;
});

51
src/index.ts

@ -1,51 +0,0 @@
import { KeyValueStorage, MemoryStorage, RefScopedStorage, ScopedStorage } from "./basic";
import { BrowserLocalStorage } from "./browser";
import { NodeDirectoryStorage } from "./node";
import { RestRemoteStorage } from "./remote";
/**
* Base type for storage usage
*/
export type TKStorage = KeyValueStorage
/**
* Exposed classes
*/
export { MemoryStorage, ScopedStorage, RefScopedStorage, BrowserLocalStorage, NodeDirectoryStorage, RestRemoteStorage };
/**
* Get the best "local" storage available
*/
export function getLocalStorage(appname: string): TKStorage {
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();
}
}
}
/**
* Get a remote storage, based on an URI
*
* If *shared* is set to true, a namespace will be used to avoid collisions, using getLocalStorage to persist it
*/
export function getRemoteStorage(appname: string, uri: string, options = { shared: false }): TKStorage {
let storage: TKStorage = new RestRemoteStorage(appname, uri);
storage = new ScopedStorage(storage, appname);
if (options.shared) {
storage = new RefScopedStorage(getLocalStorage(appname), storage);
}
return storage;
}
/**
* Get a in-memory volatile storage
*/
export function getMemoryStorage(): TKStorage {
return new MemoryStorage();
}

30
src/node.test.ts

@ -1,30 +0,0 @@
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

@ -1,82 +0,0 @@
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();
}
});
}
});
}
}

33
src/remote.test.ts

@ -1,33 +0,0 @@
import getPort from "get-port";
import { MemoryStorage } from "./basic";
import { basicCheck } from "./basic.test";
import { RestRemoteStorage } from "./remote";
import { startRestServer } from "./server";
export function startTestServer(): { port: () => number } {
let app: { close: Function } | undefined;
let port: number;
beforeEach(async () => {
port = await getPort();
app = await startRestServer(port, new MemoryStorage());
});
afterEach(async () => {
if (app) {
await app.close();
app = undefined;
}
});
return { port: () => port };
}
describe(RestRemoteStorage, () => {
const server = startTestServer();
it("communicates with the server", async () => {
const storage = new RestRemoteStorage("test", `http://localhost:${server.port()}`);
await basicCheck(storage);
});
});

64
src/remote.ts

@ -1,64 +0,0 @@
import { KeyValueStorage } from "./basic";
function protectKey(key: string): string {
return encodeURIComponent(key);
}
type RestClient = (method: "GET" | "POST" | "PUT" | "DELETE", uri: string, body?: string) => Promise<string | null>
export const HEADER_REQUESTER = "X-TK-Storage-App"
export const HEADER_REPLYIER = "X-TK-Storage-Control"
/**
* Storage on a remote server, directly usable using HTTP REST verbs
*/
export class RestRemoteStorage implements KeyValueStorage {
readonly appname: string
readonly base_url: string
readonly client: RestClient
constructor(appname: string, url: string) {
this.appname = appname;
this.base_url = (url[url.length - 1] != "/") ? url + "/" : url;
this.client = this.createClient();
}
async get(key: string): Promise<string | null> {
key = protectKey(key);
return await this.client("GET", key);
}
async set(key: string, value: string | null): Promise<void> {
key = protectKey(key);
if (value === null) {
await this.client("DELETE", key);
} else {
await this.client("PUT", key, value);
}
}
private createClient(): RestClient {
const Request: typeof XMLHttpRequest = (typeof XMLHttpRequest === "undefined") ? require("xmlhttprequest").XMLHttpRequest : XMLHttpRequest;
const client: RestClient = (method, path, body?) => new Promise((resolve, reject) => {
const xhr = new Request();
xhr.open(method, `${this.base_url}${path}`);
xhr.responseType = 'text';
xhr.setRequestHeader(HEADER_REQUESTER, this.appname);
xhr.onload = function () {
if (xhr.getResponseHeader(HEADER_REPLYIER) != "ok") {
reject("storage not compatible with tk-storage");
} else if (xhr.status == 200) {
resolve(xhr.responseText);
} else if (xhr.status == 404) {
resolve(null);
} else {
reject(`http error ${xhr.status}: ${xhr.statusText}`);
}
};
xhr.onerror = function () {
reject("unknown error");
};
xhr.send(body);
});
return client;
}
}

64
src/server.ts

@ -1,64 +0,0 @@
import bodyParser from "body-parser";
import cors from "cors";
import express from "express";
import { getLocalStorage } from ".";
import { KeyValueStorage } from "./basic";
import { HEADER_REPLYIER, HEADER_REQUESTER } from "./remote";
type Server = {
close: () => Promise<void>
}
/**
* Start a server compliant with RestRemoteStorage
*/
export function startRestServer(port: number, storage: KeyValueStorage): Promise<Server> {
const app = express();
app.use(
cors({ exposedHeaders: [HEADER_REPLYIER] }),
bodyParser.text(),
(req, res, next) => {
if (req.header(HEADER_REQUESTER)) {
res.set(HEADER_REPLYIER, "ok");
next();
} else {
res.status(400).send();
}
},
);
app.get("*", (req, res, next) => {
storage.get(req.path).then(result => {
if (result === null) {
res.status(404).send();
} else {
res.send(result);
}
}).catch(next);
});
app.put("*", (req, res, next) => {
storage.set(req.path, req.body).then(() => {
res.send();
}).catch(next);
});
app.delete("*", (req, res, next) => {
storage.set(req.path, null).then(() => {
res.send();
}).catch(next);
});
return new Promise((resolve) => {
const server = app.listen(port, () => {
resolve({
close: () => new Promise((resolve, reject) => {
server.close((err) => err ? reject(err) : resolve());
})
})
});
});
}
if (typeof require !== "undefined" && require.main === module) {
startRestServer(5001, getLocalStorage("tk-storage-server")).then(() => {
console.log(`Server running, use http://localhost:5001 to connect`);
});
}

57
testing.ts

@ -0,0 +1,57 @@
import { KeyValueStorage, MemoryStorage } from "./basic.ts";
import { expect } from "https://code.thunderk.net/typescript/devtools/raw/1.2.2/testing.ts";
export {
describe,
expect,
it,
} from "https://code.thunderk.net/typescript/devtools/raw/1.2.2/testing.ts";
import { getAvailablePort } from "https://deno.land/x/port@1.0.0/mod.ts";
import { startRestServer } from "./server.ts";
/**
* Basic high-level test suite for any kind storage
*/
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);
}
/**
* Temporarily disable localStorage (constructor will throw error),
* for the duration of the callback
*/
export async function disableLocalStorage(
body: () => Promise<void>,
): Promise<void> {
const ns = window as any;
const removed = ns["localStorage"];
delete ns["localStorage"];
try {
await body;
} finally {
ns["localStorage"] = removed;
}
}
/**
* Run a temporary test server, with memory storage
*/
export async function runTestServer(
body: (url: string) => Promise<void>,
): Promise<void> {
const port = await getAvailablePort();
if (!port) {
throw new Error("No port available for test server");
}
const app = await startRestServer(port, new MemoryStorage());
try {
await body(`http://localhost:${port}`);
} finally {
await app.close();
}
}

19
tsconfig.json

@ -1,21 +1,10 @@
{
"compilerOptions": {
"moduleResolution": "node",
"esModuleInterop": true,
"module": "esnext",
"target": "ESNext",
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"preserveConstEnums": true,
"declaration": true,
"target": "es6",
"lib": [
"dom",
"es6"
]
},
"exclude": [
"node_modules",
"dist",
"src/**/*.test.ts"
]
"preserveConstEnums": true
}
}

Loading…
Cancel
Save