Compare commits

...

1 Commits

Author SHA1 Message Date
Michaël Lemaire fd5d0c736f Use oak instead of opine 2021-09-16 23:22:24 +02:00
22 changed files with 251 additions and 135 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
deno.d.ts
.vscode
.local
.output
web/*.js

View File

@ -1,3 +1,51 @@
# typescript/storage
[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/storage?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=storage)
## About
Javascript/Typescript persistent storage, with key-value stores as foundation.
## Import
In deno:
```typescript
import { getLocalStorage } from "https://js.thunderk.net/storage/mod.ts";
```
In browser:
```html
<script type="module">
import { getLocalStorage } from "https://js.thunderk.net/storage/mod.js";
</script>
```
## Use
To get a storage locally persistent (saved in browser data or on disk for Deno):
```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
./cli.ts
```

11
cli.ts Executable file
View File

@ -0,0 +1,11 @@
#!./run
import { getLocalStorage } from "./src/main.ts";
import { startRestServer } from "./src/server.ts";
const PORT = 5001;
if (import.meta.main) {
await startRestServer(PORT, getLocalStorage("tk-storage-server"));
console.log(`Server running, use http://localhost:${PORT} to connect`);
}

View File

@ -1,4 +1,4 @@
export { readAll } from "https://deno.land/std@0.104.0/io/util.ts";
export { readAll } from "https://deno.land/std@0.107.0/io/util.ts";
export { json, opine } from "https://deno.land/x/opine@1.7.2/mod.ts";
export { opineCors } from "https://deno.land/x/cors@v1.2.2/mod.ts";
export { Application } from "https://deno.land/x/oak@v9.0.1/mod.ts";
export { oakCors } from "https://deno.land/x/cors@v1.2.2/mod.ts";

View File

@ -2,6 +2,4 @@ export {
describe,
expect,
it,
} from "https://js.thunderk.net/devtools@1.3.0/testing.ts";
export { getAvailablePort } from "https://deno.land/x/port@1.0.0/mod.ts";
} from "https://js.thunderk.net/testing@1.0.0/mod.ts";

View File

@ -1 +1 @@
export { v1 as uuid1 } from "https://deno.land/std@0.104.0/uuid/mod.ts";
export { v1 as uuid1 } from "https://deno.land/std@0.107.0/uuid/mod.ts";

3
doc/about.md Normal file
View File

@ -0,0 +1,3 @@
## About
Javascript/Typescript persistent storage, with key-value stores as foundation.

15
doc/import.md Normal file
View File

@ -0,0 +1,15 @@
## Import
In deno:
```typescript
import { getLocalStorage } from "https://js.thunderk.net/storage/mod.ts";
```
In browser:
```html
<script type="module">
import { getLocalStorage } from "https://js.thunderk.net/storage/mod.js";
</script>
```

3
doc/index Normal file
View File

@ -0,0 +1,3 @@
about
import
use

27
doc/use.md Normal file
View File

@ -0,0 +1,27 @@
## Use
To get a storage locally persistent (saved in browser data or on disk for Deno):
```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
./cli.ts
```

59
mod.ts
View File

@ -3,61 +3,24 @@ import {
MemoryStorage,
RefScopedStorage,
ScopedStorage,
} from "./basic.ts";
import { LocalStorage } from "./local.ts";
import { RestRemoteStorage } from "./remote.ts";
} from "./src/basic.ts";
import {
getLocalStorage,
getMemoryStorage,
getRemoteStorage,
} from "./src/main.ts";
import { LocalStorage } from "./src/local.ts";
import { RestRemoteStorage } from "./src/remote.ts";
/**
* Base type for storage usage
*/
export type TKStorage = KeyValueStorage;
/**
* Exposed classes
*/
export {
getLocalStorage,
getMemoryStorage,
getRemoteStorage,
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();
}

View File

@ -1,70 +0,0 @@
#!./run
import { KeyValueStorage } from "./basic.ts";
import { json, opine, opineCors, readAll } from "./deps.server.ts";
import { getLocalStorage } from "./mod.ts";
import { HEADER_REPLYIER, HEADER_REQUESTER } from "./remote.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: async () => {
server.close();
},
});
});
});
}
if (import.meta.main) {
await startRestServer(PORT, getLocalStorage("tk-storage-server"));
console.log(`Server running, use http://localhost:${PORT} to connect`);
}

View File

@ -1,4 +1,4 @@
import { uuid1 } from "./deps.ts";
import { uuid1 } from "../deps.ts";
/**
* Standard interface for the simplest key-value store

View File

@ -1,4 +1,4 @@
import { getLocalStorage, getMemoryStorage, getRemoteStorage } from "./mod.ts";
import { getLocalStorage, getMemoryStorage, getRemoteStorage } from "./main.ts";
import { MemoryStorage, ScopedStorage } from "./basic.ts";
import { describe, expect, it, runTestServer } from "./testing.ts";

63
src/main.ts Normal file
View File

@ -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();
}

56
src/server.ts Normal file
View File

@ -0,0 +1,56 @@
import { KeyValueStorage } from "./basic.ts";
import { Application, oakCors, readAll } from "../deps.server.ts";
import { HEADER_REPLYIER, HEADER_REQUESTER } from "./remote.ts";
type Server = {
close: () => Promise<void>;
};
/**
* Start a server compliant with RestRemoteStorage
*/
export function startRestServer(
port: number,
storage: KeyValueStorage,
): Server {
const app = new Application();
const controller = new AbortController();
app.use(oakCors());
app.use(async (context) => {
if (context.request.headers.get(HEADER_REQUESTER)) {
context.response.headers.set(HEADER_REPLYIER, "ok");
const method = context.request.method;
const path = context.request.url.pathname;
if (method == "GET") {
const result = await storage.get(path);
if (result === null) {
context.response.status = 404;
} else {
context.response.body = result;
}
} else if (method == "PUT") {
const body = context.request.body({ type: "reader" });
const data = await readAll(body.value);
const value = new TextDecoder().decode(data);
await storage.set(path, value);
} else if (method == "DELETE") {
await storage.set(path, null);
} else {
context.response.status = 405;
}
} else {
context.response.status = 400;
}
});
const listen = app.listen({ port, signal: controller.signal });
return {
close: async () => {
controller.abort();
await listen;
},
};
}

View File

@ -1,9 +1,11 @@
import { KeyValueStorage, MemoryStorage } from "./basic.ts";
import { startRestServer } from "./server.ts";
import { describe, expect, getAvailablePort, it } from "./deps.testing.ts";
import { describe, expect, it } from "../deps.testing.ts";
export { describe, expect, it };
const PORT = 5002;
/**
* Basic high-level test suite for any kind storage
*/
@ -39,14 +41,9 @@ export async function disableLocalStorage(
export async function runTestServer(
body: (url: string) => Promise<void>,
): Promise<void> {
const port = await getAvailablePort({ port: { start: 3000, end: 30000 } });
if (!port) {
throw new Error("No port available for test server");
}
const app = await startRestServer(port, new MemoryStorage());
const app = await startRestServer(PORT, new MemoryStorage());
try {
await body(`http://localhost:${port}`);
await body(`http://localhost:${PORT}`);
} finally {
await app.close();
}