diff --git a/README.md b/README.md
index 110e639..4dffb13 100644
--- a/README.md
+++ b/README.md
@@ -102,24 +102,24 @@ The library runs in the browser via esm.sh — no build step required:
```html
```
diff --git a/cli/main.tsx b/cli/main.tsx
index 4ebdd2c..0d5ecb5 100644
--- a/cli/main.tsx
+++ b/cli/main.tsx
@@ -20,23 +20,38 @@ type Shortlinks = Record;
class GoService {
public constructor(
- private readonly kv: Deno.Kv,
+ private readonly kv: Deno.Kv | undefined,
+ private readonly staticShortlinks: Shortlinks = {},
private readonly kvNamespace: Deno.KvKey = ["go"],
) {}
+ public get writable(): boolean {
+ return this.kv !== undefined;
+ }
+
+ private requireKv(): Deno.Kv {
+ if (!this.kv) {
+ throw new Error(
+ "Shortlinks are read-only: no Deno KV database is bound to this deployment.",
+ );
+ }
+ return this.kv;
+ }
+
public async add(
alias: string,
destination: string,
force = false,
): Promise {
- const shortlinksResult = await this.kv.get(this.kvNamespace);
+ const kv = this.requireKv();
+ const shortlinksResult = await kv.get(this.kvNamespace);
const shortlinks = shortlinksResult.value ?? {};
if (!force && shortlinks[alias]) {
throw new Error("Shortlink already exists.");
}
shortlinks[alias] = destination;
- const result = await this.kv.atomic()
+ const result = await kv.atomic()
.check(shortlinksResult)
.set(this.kvNamespace, shortlinks)
.commit();
@@ -46,14 +61,15 @@ class GoService {
}
public async delete(alias: string): Promise {
- const shortlinksResult = await this.kv.get(this.kvNamespace);
+ const kv = this.requireKv();
+ const shortlinksResult = await kv.get(this.kvNamespace);
const shortlinks = shortlinksResult.value ?? {};
if (!shortlinks[alias]) {
throw new Error("Shortlink does not exist.");
}
delete shortlinks[alias];
- const result = await this.kv.atomic()
+ const result = await kv.atomic()
.check(shortlinksResult)
.set(this.kvNamespace, shortlinks)
.commit();
@@ -63,8 +79,11 @@ class GoService {
}
public async shortlinks(): Promise {
+ if (!this.kv) {
+ return this.staticShortlinks;
+ }
const shortlinksResult = await this.kv.get(this.kvNamespace);
- return shortlinksResult.value ?? {};
+ return { ...this.staticShortlinks, ...(shortlinksResult.value ?? {}) };
}
}
@@ -85,6 +104,15 @@ function GoRouter(props: { service: GoService }) {
if (!isAuthorized(ctx.request.headers)) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
+ if (!props.service.writable) {
+ return Response.json(
+ {
+ error:
+ "Shortlinks are read-only: no Deno KV database is bound.",
+ },
+ { status: 503 },
+ );
+ }
const body = await ctx.request.json();
await props.service.add(body.alias, body.destination, body.force);
@@ -100,6 +128,15 @@ function GoRouter(props: { service: GoService }) {
if (!isAuthorized(ctx.request.headers)) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
+ if (!props.service.writable) {
+ return Response.json(
+ {
+ error:
+ "Shortlinks are read-only: no Deno KV database is bound.",
+ },
+ { status: 503 },
+ );
+ }
const body = await ctx.request.json();
await props.service.delete(body.alias);
@@ -183,6 +220,12 @@ function GoRouter(props: { service: GoService }) {
const shortlinks = await props.service.shortlinks();
const url = new URL(ctx.request.url);
const destination = go(url, shortlinks);
+ if (destination.href === url.href) {
+ return Response.json(
+ { error: `Shortlink not found: ${url.pathname}` },
+ { status: 404 },
+ );
+ }
return new Response(
null,
{
@@ -196,9 +239,39 @@ function GoRouter(props: { service: GoService }) {
);
}
+const staticShortlinks: Shortlinks = JSON.parse(
+ Deno.readTextFileSync(new URL("../links.json", import.meta.url)),
+);
+
+async function openKvIfAvailable(): Promise {
+ try {
+ return await Deno.openKv();
+ } catch (error) {
+ console.warn(
+ "Deno KV is unavailable, serving links.json read-only:",
+ String(error),
+ );
+ return undefined;
+ }
+}
+
+const kv = await openKvIfAvailable();
+
+let serviceRouter: ReturnType | undefined;
+
+function getServiceRouter(): ReturnType {
+ if (!serviceRouter) {
+ serviceRouter = ;
+ }
+ return serviceRouter;
+}
+
+export default {
+ fetch: (request: Request) => getServiceRouter().fetch(request),
+} satisfies Deno.ServeDefaultExport;
+
if (import.meta.main) {
- const kv = await Deno.openKv();
- const goService = new GoService(kv);
- const router = ;
- Deno.serve((request) => router.fetch(request));
+ const port = parseInt(Deno.env.get("PORT") ?? "8000");
+ console.info(`go.fart.tools development server: http://localhost:${port}/`);
+ Deno.serve({ port }, (request) => getServiceRouter().fetch(request));
}
diff --git a/demo/browser.html b/demo/browser.html
index 6e0b7f2..e10af27 100644
--- a/demo/browser.html
+++ b/demo/browser.html
@@ -4,54 +4,53 @@
go — browser demo