Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,24 +102,24 @@ The library runs in the browser via esm.sh — no build step required:

```html
<script id="shortlinks" type="application/json">
{
"github": "https://github.com"
}
{
"github": "https://github.com"
}
</script>
<script type="importmap">
{
"imports": {
"@fartlabs/go": "https://esm.sh/jsr/@fartlabs/go@0.0.3"
}
{
"imports": {
"@fartlabs/go": "https://esm.sh/jsr/@fartlabs/go@0.0.3"
}
}
</script>
<script type="module">
import { go } from "@fartlabs/go";
const shortlinks = JSON.parse(
document.getElementById("shortlinks").textContent,
);
const result = go(new URL(location.href), shortlinks);
// ...
import { go } from "@fartlabs/go";
const shortlinks = JSON.parse(
document.getElementById("shortlinks").textContent,
);
const result = go(new URL(location.href), shortlinks);
// ...
</script>
```

Expand Down
93 changes: 83 additions & 10 deletions cli/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,38 @@ type Shortlinks = Record<string, string>;

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<void> {
const shortlinksResult = await this.kv.get<Shortlinks>(this.kvNamespace);
const kv = this.requireKv();
const shortlinksResult = await kv.get<Shortlinks>(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();
Expand All @@ -46,14 +61,15 @@ class GoService {
}

public async delete(alias: string): Promise<void> {
const shortlinksResult = await this.kv.get<Shortlinks>(this.kvNamespace);
const kv = this.requireKv();
const shortlinksResult = await kv.get<Shortlinks>(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();
Expand All @@ -63,8 +79,11 @@ class GoService {
}

public async shortlinks(): Promise<Shortlinks> {
if (!this.kv) {
return this.staticShortlinks;
}
const shortlinksResult = await this.kv.get<Shortlinks>(this.kvNamespace);
return shortlinksResult.value ?? {};
return { ...this.staticShortlinks, ...(shortlinksResult.value ?? {}) };
}
}

Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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,
{
Expand All @@ -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<Deno.Kv | undefined> {
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<typeof GoRouter> | undefined;

function getServiceRouter(): ReturnType<typeof GoRouter> {
if (!serviceRouter) {
serviceRouter = <GoRouter service={new GoService(kv, staticShortlinks)} />;
}
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 = <GoRouter service={goService} />;
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));
}
81 changes: 40 additions & 41 deletions demo/browser.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,53 @@
<meta charset="UTF-8" />
<title>go — browser demo</title>
<script type="importmap">
{
"imports": {
"@fartlabs/go": "https://esm.sh/jsr/@fartlabs/go@0.0.3"
}
{
"imports": {
"@fartlabs/go": "https://esm.sh/jsr/@fartlabs/go@0.0.3"
}
}
</script>
<script id="shortlinks" type="application/json">
{
"github": "https://github.com",
"docs": "/documentation",
"docs/api": "/reference"
}
{
"github": "https://github.com",
"docs": "/documentation",
"docs/api": "/reference"
}
</script>
</head>
<body>
<script type="module">
import { go } from "@fartlabs/go";
const shortlinks = JSON.parse(
document.getElementById("shortlinks").textContent,
);
const url = new URL(window.location.href);
const result = go(url, shortlinks);
if (result.href !== url.href) {
// Redirect via meta-refresh
document.open();
document.write(`<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0;url=${result.href}">
<title>Redirecting…</title>
</head>
<body>
<p>Resolving <code>${url.href}</code> → <a href="${result.href}">${result.href}</a></p>
</body>
</html>`);
document.close();
} else {
// Render shortlink directory listing.
const entries = Object.entries(shortlinks);
document.body.innerHTML = `<ul>${
entries
.map(
([k, v]) =>
`<li><code>${k}</code> → <a href="?${k}">${v}</a></li>`,
)
.join("")
}</ul>`;
}
import { go } from "@fartlabs/go";
const shortlinks = JSON.parse(
document.getElementById("shortlinks").textContent,
);
const url = new URL(window.location.href);
const result = go(url, shortlinks);
if (result.href !== url.href) {
// Redirect via meta-refresh
document.open();
document.write(`<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0;url=${result.href}">
<title>Redirecting…</title>
</head>
<body>
<p>Resolving <code>${url.href}</code> → <a href="${result.href}">${result.href}</a></p>
</body>
</html>`);
document.close();
} else {
// Render shortlink directory listing.
const entries = Object.entries(shortlinks);
document.body.innerHTML = `<ul>${
entries
.map(
([k, v]) => `<li><code>${k}</code> → <a href="?${k}">${v}</a></li>`,
)
.join("")
}</ul>`;
}
</script>
</body>
</html>
4 changes: 4 additions & 0 deletions links.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"api": "https://api.example.com",
"chat": "https://discord.gg/ZwHaZ63yq3",
"docs": "/documentation",
"github": "https://github.com",
"in": "https://linkedin.com/company/fartlabs",
"instagram": "https://instagram.com/fartlabs",
"twitter": "https://x.com/fartlabs_org",
"yt": "https://youtube.com"
}
Loading