diff --git a/.gitignore b/.gitignore index d69a4c0..8b7abdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ deno.d.ts .vscode .local +.output +web/*.js diff --git a/README.md b/README.md index 90933e1..e623b0c 100644 --- a/README.md +++ b/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 + +``` + +## 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 +``` diff --git a/cli.ts b/cli.ts new file mode 100755 index 0000000..2aeb3f6 --- /dev/null +++ b/cli.ts @@ -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`); +} diff --git a/deps.server.ts b/deps.server.ts index 8572aac..bcd19be 100644 --- a/deps.server.ts +++ b/deps.server.ts @@ -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"; diff --git a/deps.testing.ts b/deps.testing.ts index d69baa3..769812b 100644 --- a/deps.testing.ts +++ b/deps.testing.ts @@ -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"; diff --git a/deps.ts b/deps.ts index 15995d3..21bd81b 100644 --- a/deps.ts +++ b/deps.ts @@ -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"; diff --git a/doc/about.md b/doc/about.md new file mode 100644 index 0000000..103f5b3 --- /dev/null +++ b/doc/about.md @@ -0,0 +1,3 @@ +## About + +Javascript/Typescript persistent storage, with key-value stores as foundation. diff --git a/doc/import.md b/doc/import.md new file mode 100644 index 0000000..c288c49 --- /dev/null +++ b/doc/import.md @@ -0,0 +1,15 @@ +## Import + +In deno: + +```typescript +import { getLocalStorage } from "https://js.thunderk.net/storage/mod.ts"; +``` + +In browser: + +```html + +``` diff --git a/doc/index b/doc/index new file mode 100644 index 0000000..b1dffcc --- /dev/null +++ b/doc/index @@ -0,0 +1,3 @@ +about +import +use diff --git a/doc/use.md b/doc/use.md new file mode 100644 index 0000000..41e6d27 --- /dev/null +++ b/doc/use.md @@ -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 +``` diff --git a/mod.ts b/mod.ts index 81ed16f..a975a96 100644 --- a/mod.ts +++ b/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(); -} diff --git a/server.ts b/server.ts deleted file mode 100755 index 1ee6be1..0000000 --- a/server.ts +++ /dev/null @@ -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; -}; - -/** - * Start a server compliant with RestRemoteStorage - */ -export function startRestServer( - port: number, - storage: KeyValueStorage, -): Promise { - 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`); -} diff --git a/basic.test.ts b/src/basic.test.ts similarity index 100% rename from basic.test.ts rename to src/basic.test.ts diff --git a/basic.ts b/src/basic.ts similarity index 98% rename from basic.ts rename to src/basic.ts index 6a0e261..dd5035e 100644 --- a/basic.ts +++ b/src/basic.ts @@ -1,4 +1,4 @@ -import { uuid1 } from "./deps.ts"; +import { uuid1 } from "../deps.ts"; /** * Standard interface for the simplest key-value store diff --git a/local.test.ts b/src/local.test.ts similarity index 100% rename from local.test.ts rename to src/local.test.ts diff --git a/local.ts b/src/local.ts similarity index 100% rename from local.ts rename to src/local.ts diff --git a/mod.test.ts b/src/main.test.ts similarity index 99% rename from mod.test.ts rename to src/main.test.ts index f72b196..8f5ccce 100644 --- a/mod.test.ts +++ b/src/main.test.ts @@ -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"; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..81ed16f --- /dev/null +++ b/src/main.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(); +} diff --git a/remote.test.ts b/src/remote.test.ts similarity index 100% rename from remote.test.ts rename to src/remote.test.ts diff --git a/remote.ts b/src/remote.ts similarity index 100% rename from remote.ts rename to src/remote.ts diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..3b636b7 --- /dev/null +++ b/src/server.ts @@ -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; +}; + +/** + * 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; + }, + }; +} diff --git a/testing.ts b/src/testing.ts similarity index 77% rename from testing.ts rename to src/testing.ts index 0b17174..1f99510 100644 --- a/testing.ts +++ b/src/testing.ts @@ -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, ): Promise { - 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(); }