Compare commits
No commits in common. "master" and "1.0.1" have entirely different histories.
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,5 +1,3 @@
|
||||||
deno.d.ts
|
deno.d.ts
|
||||||
.vscode
|
.vscode
|
||||||
.local
|
.local
|
||||||
.output
|
|
||||||
web/*.js
|
|
||||||
|
|
48
README.md
48
README.md
|
@ -1,51 +1,3 @@
|
||||||
# typescript/storage
|
# typescript/storage
|
||||||
|
|
||||||
[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/storage?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=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
|
|
||||||
```
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { uuid1 } from "../deps.ts";
|
import { uuid1 } from "./deps.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard interface for the simplest key-value store
|
* Standard interface for the simplest key-value store
|
11
cli.ts
11
cli.ts
|
@ -1,11 +0,0 @@
|
||||||
#!./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,4 +1,4 @@
|
||||||
export { readAll } from "https://deno.land/std@0.107.0/io/util.ts";
|
export { readAll } from "https://deno.land/std@0.104.0/io/util.ts";
|
||||||
|
|
||||||
export { Application } from "https://deno.land/x/oak@v9.0.1/mod.ts";
|
export { json, opine } from "https://deno.land/x/opine@1.7.2/mod.ts";
|
||||||
export { oakCors } from "https://deno.land/x/cors@v1.2.2/mod.ts";
|
export { opineCors } from "https://deno.land/x/cors@v1.2.2/mod.ts";
|
||||||
|
|
|
@ -2,4 +2,6 @@ export {
|
||||||
describe,
|
describe,
|
||||||
expect,
|
expect,
|
||||||
it,
|
it,
|
||||||
} from "https://js.thunderk.net/testing@1.0.0/mod.ts";
|
} from "https://js.thunderk.net/devtools@1.3.0/testing.ts";
|
||||||
|
|
||||||
|
export { getAvailablePort } from "https://deno.land/x/port@1.0.0/mod.ts";
|
||||||
|
|
2
deps.ts
2
deps.ts
|
@ -1 +1 @@
|
||||||
export { v1 as uuid1 } from "https://deno.land/std@0.107.0/uuid/mod.ts";
|
export { v1 as uuid1 } from "https://deno.land/std@0.104.0/uuid/mod.ts";
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
## About
|
|
||||||
|
|
||||||
Javascript/Typescript persistent storage, with key-value stores as foundation.
|
|
|
@ -1,15 +0,0 @@
|
||||||
## 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
27
doc/use.md
|
@ -1,27 +0,0 @@
|
||||||
## 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
|
|
||||||
```
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getLocalStorage, getMemoryStorage, getRemoteStorage } from "./main.ts";
|
import { getLocalStorage, getMemoryStorage, getRemoteStorage } from "./mod.ts";
|
||||||
import { MemoryStorage, ScopedStorage } from "./basic.ts";
|
import { MemoryStorage, ScopedStorage } from "./basic.ts";
|
||||||
import { describe, expect, it, runTestServer } from "./testing.ts";
|
import { describe, expect, it, runTestServer } from "./testing.ts";
|
||||||
|
|
59
mod.ts
59
mod.ts
|
@ -3,24 +3,61 @@ import {
|
||||||
MemoryStorage,
|
MemoryStorage,
|
||||||
RefScopedStorage,
|
RefScopedStorage,
|
||||||
ScopedStorage,
|
ScopedStorage,
|
||||||
} from "./src/basic.ts";
|
} from "./basic.ts";
|
||||||
import {
|
import { LocalStorage } from "./local.ts";
|
||||||
getLocalStorage,
|
import { RestRemoteStorage } from "./remote.ts";
|
||||||
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;
|
export type TKStorage = KeyValueStorage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposed classes
|
||||||
|
*/
|
||||||
export {
|
export {
|
||||||
getLocalStorage,
|
|
||||||
getMemoryStorage,
|
|
||||||
getRemoteStorage,
|
|
||||||
LocalStorage,
|
LocalStorage,
|
||||||
MemoryStorage,
|
MemoryStorage,
|
||||||
RefScopedStorage,
|
RefScopedStorage,
|
||||||
RestRemoteStorage,
|
RestRemoteStorage,
|
||||||
ScopedStorage,
|
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();
|
||||||
|
}
|
||||||
|
|
70
server.ts
Executable file
70
server.ts
Executable file
|
@ -0,0 +1,70 @@
|
||||||
|
#!./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`);
|
||||||
|
}
|
63
src/main.ts
63
src/main.ts
|
@ -1,63 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
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,11 +1,9 @@
|
||||||
import { KeyValueStorage, MemoryStorage } from "./basic.ts";
|
import { KeyValueStorage, MemoryStorage } from "./basic.ts";
|
||||||
import { startRestServer } from "./server.ts";
|
import { startRestServer } from "./server.ts";
|
||||||
import { describe, expect, it } from "../deps.testing.ts";
|
import { describe, expect, getAvailablePort, it } from "./deps.testing.ts";
|
||||||
|
|
||||||
export { describe, expect, it };
|
export { describe, expect, it };
|
||||||
|
|
||||||
const PORT = 5002;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic high-level test suite for any kind storage
|
* Basic high-level test suite for any kind storage
|
||||||
*/
|
*/
|
||||||
|
@ -41,9 +39,14 @@ export async function disableLocalStorage(
|
||||||
export async function runTestServer(
|
export async function runTestServer(
|
||||||
body: (url: string) => Promise<void>,
|
body: (url: string) => Promise<void>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const app = await startRestServer(PORT, new MemoryStorage());
|
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());
|
||||||
try {
|
try {
|
||||||
await body(`http://localhost:${PORT}`);
|
await body(`http://localhost:${port}`);
|
||||||
} finally {
|
} finally {
|
||||||
await app.close();
|
await app.close();
|
||||||
}
|
}
|
Loading…
Reference in a new issue