Skip to content

Commit

Permalink
feat: add caching and cron image refresh
Browse files Browse the repository at this point in the history
  • Loading branch information
Jabolol committed Dec 15, 2023
1 parent 9a61c09 commit 3ecd205
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 10 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

# brownie

Add support for **Google Analytics** to your GitHub readme.
Add support for **Google Analytics** to your GitHub readme and cache your images
for a faster loading experience!

## getting started

Expand All @@ -24,24 +25,31 @@ git clone [email protected]:Jabolol/brownie.git .
cp .env.example .env && vim .env
```

4. Edit the [`config.ts`](./config.ts) file to add your images:
4. Edit the [`config.ts`](./config.ts) file to add your images and the cron
schedule to cache the images:

```ts
export const config: RouteConfig = {
routes: {
cocoa: "https://my-stats.dev/contribs",
vanilla: "https://my-stats.dev/issues",
},
schedule: {
minute: {
every: 20,
},
},
};
```

5. This will make the endpoints `/cocoa` and `/vanilla` available
5. This will make the endpoints `/cocoa` and `/vanilla` available and cache the
images every 20 minutes. You can add as many routes as you want!

> [!NOTE]
> It is common practice to make your routes ingredients of your brownie!
6. Deploy to [Deno Deploy](https://deno.com/deploy) and fill in the environment
variables:
variables

7. Add your freshly baked images to your readme

Expand Down
13 changes: 12 additions & 1 deletion config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
interface RouteConfig {
/**
* Route mapping configuration object.
*/
routes: Record<string, string>;
/**
* Cron schedule configuration object.
*/
schedule: Deno.CronSchedule;
}

/**
* Route mapping configuration object.
* The convention is to use brownie ingredients as paths.
*/
export const config: RouteConfig = {
Expand All @@ -13,4 +19,9 @@ export const config: RouteConfig = {
vanilla:
"https://github-readme-streak-stats-eight-iota.vercel.app/?user=Jabolol&theme=dracula&hide_border=false",
},
schedule: {
minute: {
every: 20,
},
},
};
73 changes: 68 additions & 5 deletions main.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,74 @@
import { report } from "~/utils.ts";
import { config } from "~/config.ts";
import { function as func, option, task, taskEither } from "fp-ts";
import {
array,
either,
function as func,
option,
task,
taskEither,
} from "fp-ts";

const kv = await Deno.openKv();

/**
* This cron job retrieves the bytes of the images from the URLs specified in the configuration object and stores them in the KV store.
*/
const cron = async () => {
const raw: either.Either<Error, [string, ArrayBuffer]>[] = await func.pipe(
config.routes,
Object.entries<string>,
array.map(
([key, value]) => [key, retrieve(value)] as const,
),
array.map(([key, value]) =>
func.pipe(
value,
taskEither.map((bytes) => [key, bytes] as const),
)
),
array.sequence(task.ApplicativeSeq),
)();

const result = func.pipe(
raw,
array.map(
either.map(
([key, value]) => kv.set(["cache", key], value),
),
),
array.sequence(either.Applicative),
either.match(
(error) => {
throw new Error(`Failed to process array: ${error}`);
},
(elem: Promise<Deno.KvCommitResult>[]) => elem,
),
);

await Promise.all(result);
};

/**
* Fetches the bytes from the specified URL.
* @param url - The URL to fetch the bytes from.
* @returns A promise that resolves to an ArrayBuffer.
*/
// TODO: fpts-ify this function
const fetchBytes = async (url: string) => {
const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch resource");
return response.arrayBuffer();
const path = new URL(url).pathname.slice(1);
const { value } = await kv.get<ArrayBuffer>(["cache", path]);
if (!value) {
console.warn(`Cache miss => ${url}`);
const response = await fetch(url);
const bytes = await response.arrayBuffer();
await kv.set(["cache", path], bytes);
return bytes;
} else {
console.log(`Cache hit => ${url}`);
}

return value;
};

/**
Expand All @@ -31,6 +89,7 @@ const retrieve = func.flow(
* @param request - The request object.
* @returns The path of the request URL.
*/
// TODO: remove duplication with `retrieve`
const path = func.flow(
(request: Request) => new URL(request.url),
(url) => url.pathname,
Expand Down Expand Up @@ -73,4 +132,8 @@ const handler = (request: Request, ctx: Deno.ServeHandlerInfo) =>
task.map((response) => report(request, response, ctx)),
)();

Deno.serve(handler);
Deno.cron("refresh kv store", config.schedule, cron);

if (import.meta.main) {
Deno.serve(handler);
}

1 comment on commit 3ecd205

@deno-deploy
Copy link

@deno-deploy deno-deploy bot commented on 3ecd205 Dec 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed to deploy:

UNCAUGHT_EXCEPTION

TypeError: Invalid cron schedule
    at Object.cron (ext:deno_cron/01_cron.ts:25:24)
    at file:///src/main.ts:135:6

Please sign in to comment.