Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
Michaël Lemaire | fd5d0c736f | ||
Michaël Lemaire | 75487c3278 | ||
Michaël Lemaire | 9c8aa11dea | ||
Michaël Lemaire | bc885f4612 |
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
deno.d.ts
|
||||
.vscode
|
||||
.local
|
||||
.output
|
||||
web/*.js
|
||||
|
|
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"deno.enable": true,
|
||||
"deno.suggest.imports.hosts": {
|
||||
"https://deno.land": false
|
||||
}
|
||||
}
|
48
README.md
48
README.md
|
@ -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
11
cli.ts
Executable 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`);
|
||||
}
|
1
config/run.flags
Normal file
1
config/run.flags
Normal file
|
@ -0,0 +1 @@
|
|||
--allow-read --allow-net --location https://rs.thunderk.net/
|
1
config/test.flags
Normal file
1
config/test.flags
Normal file
|
@ -0,0 +1 @@
|
|||
--allow-read --allow-net --location https://test.rs.thunderk.net/
|
4
deps.server.ts
Normal file
4
deps.server.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export { readAll } from "https://deno.land/std@0.107.0/io/util.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";
|
5
deps.testing.ts
Normal file
5
deps.testing.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export {
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
} from "https://js.thunderk.net/testing@1.0.0/mod.ts";
|
5
deps.ts
5
deps.ts
|
@ -1,4 +1 @@
|
|||
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";
|
||||
export { v1 as uuid1 } from "https://deno.land/std@0.107.0/uuid/mod.ts";
|
||||
|
|
3
doc/about.md
Normal file
3
doc/about.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## About
|
||||
|
||||
Javascript/Typescript persistent storage, with key-value stores as foundation.
|
15
doc/import.md
Normal file
15
doc/import.md
Normal 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>
|
||||
```
|
27
doc/use.md
Normal file
27
doc/use.md
Normal 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
59
mod.ts
|
@ -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();
|
||||
}
|
||||
|
|
19
run
Executable file
19
run
Executable file
|
@ -0,0 +1,19 @@
|
|||
#!/bin/sh
|
||||
# Simplified run tool for deno commands
|
||||
|
||||
if test $# -eq 0
|
||||
then
|
||||
echo "Usage: $0 [file or command]"
|
||||
exit 1
|
||||
elif echo $1 | grep -q '.*.ts'
|
||||
then
|
||||
denocmd=run
|
||||
denoargs=$1
|
||||
shift
|
||||
else
|
||||
denocmd=$1
|
||||
shift
|
||||
fi
|
||||
|
||||
denoargs="$(cat config/$denocmd.flags 2> /dev/null) $denoargs $@"
|
||||
exec deno $denocmd $denoargs
|
71
server.ts
71
server.ts
|
@ -1,71 +0,0 @@
|
|||
#!/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`);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { uuid1 } from "./deps.ts";
|
||||
import { uuid1 } from "../deps.ts";
|
||||
|
||||
/**
|
||||
* Standard interface for the simplest key-value store
|
|
@ -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
63
src/main.ts
Normal 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();
|
||||
}
|
|
@ -49,10 +49,14 @@ export class RestRemoteStorage implements KeyValueStorage {
|
|||
[HEADER_REQUESTER]: this.appname,
|
||||
},
|
||||
});
|
||||
// the body is consumed here to not leak resource, but it would
|
||||
// be better to consume it only when needed, once
|
||||
// https://github.com/denoland/deno/issues/4735 is fixed
|
||||
const text = await response.text();
|
||||
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();
|
||||
return text;
|
||||
} else if (response.status == 404) {
|
||||
return null;
|
||||
} else {
|
56
src/server.ts
Normal file
56
src/server.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,12 +1,10 @@
|
|||
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";
|
||||
import { describe, expect, it } from "../deps.testing.ts";
|
||||
|
||||
export { describe, expect, it };
|
||||
|
||||
const PORT = 5002;
|
||||
|
||||
/**
|
||||
* Basic high-level test suite for any kind storage
|
||||
|
@ -31,7 +29,7 @@ export async function disableLocalStorage(
|
|||
const removed = ns["localStorage"];
|
||||
delete ns["localStorage"];
|
||||
try {
|
||||
await body;
|
||||
await body();
|
||||
} finally {
|
||||
ns["localStorage"] = removed;
|
||||
}
|
||||
|
@ -43,14 +41,9 @@ export async function disableLocalStorage(
|
|||
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());
|
||||
const app = await startRestServer(PORT, new MemoryStorage());
|
||||
try {
|
||||
await body(`http://localhost:${port}`);
|
||||
await body(`http://localhost:${PORT}`);
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
Loading…
Reference in a new issue