Skip to content

Commit

Permalink
Scheduled cache busting can be built into the R2 Objects with R2 rules
Browse files Browse the repository at this point in the history
  • Loading branch information
JacobMGEvans committed Sep 1, 2023
1 parent 4a03a5c commit ed26ce2
Show file tree
Hide file tree
Showing 5 changed files with 27 additions and 64 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules
.env
dist
.wrangler
.turbo
.turbo
.dev.vars
49 changes: 8 additions & 41 deletions src/autoCacheBust.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,17 @@
export function isOlderThan(date: Date, hours: number | string) {
const now = new Date();
// For scheduled R2 cache busting, the R2 lifecycle rules can be used to delete an object after a certain amount of time.

const diffInMilliseconds = now.getTime() - date.getTime();

const diffInHours = diffInMilliseconds / 1000 / 60 / 60;

return diffInHours >= Number(hours);
}

const RECORDS_BATCH_SIZE = 500;

/**
* Creates a cache object with two methods: add and getKeys.
* */
function r2CacheCollector() {
const keys: string[] = [];
return {
add: function (key: string) {
keys.push(key);
},
getKeys: function () {
return keys;
},
};
}

async function deleteKeys(env: Env, cacheDeletion: ReturnType<typeof r2CacheCollector>) {
if (cacheDeletion.getKeys().length > 0) {
await env.R2_ARTIFACT_ARCHIVE.delete(cacheDeletion.getKeys());
}
}

async function processList(list: R2Objects, env: Env) {
const r2CacheToDelete = r2CacheCollector();
async function bustEntireCache(list: R2Objects, env: Env) {
for (const object of list.objects) {
if (isOlderThan(object.uploaded, env.EXPIRATION_HOURS)) {
r2CacheToDelete.add(object.key);
}
await env.R2_ARTIFACT_ARCHIVE.delete(object.key);
}

await deleteKeys(env, r2CacheToDelete);
}

export async function bustOldCache(env: Env, cursor?: string) {
const list = await env.R2_ARTIFACT_ARCHIVE.list({ limit: RECORDS_BATCH_SIZE, cursor });
await processList(list, env);
const list = await env.R2_ARTIFACT_ARCHIVE.list({
limit: 500,
cursor,
});
await bustEntireCache(list, env);

if (list.truncated) {
await bustOldCache(env, list.cursor);
Expand Down
4 changes: 0 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { bustOldCache } from './autoCacheBust';
import { router } from './routes';

export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return router.fetch(request, env, ctx);
},
async scheduled(_event: ScheduledEvent, env: Env, _ctx: ExecutionContext) {
await bustOldCache(env);
},
};
24 changes: 15 additions & 9 deletions src/routes.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { zValidator } from '@hono/zod-validator';
import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
import { bearerAuth } from 'hono/bearer-auth';
import { cors } from 'hono/cors';
import { HTTPException } from 'hono/http-exception';
import { z } from 'zod';
import { bustOldCache } from './autoCacheBust';

export const router = new Hono<{ Bindings: Env }>();

const paramValidator = z.object({ artifactID: z.string(), teamID: z.string().optional() });
const queryValidator = z.object({ teamId: z.string().optional(), slug: z.string().optional() });
const paramValidator = z.object({
artifactID: z.string(),
teamID: z.string().optional(),
});
const queryValidator = z.object({
teamId: z.string().optional(),
slug: z.string().optional(),
});

router.onError((error, c) => {
if (error instanceof HTTPException) {
Expand All @@ -21,19 +27,19 @@ router.onError((error, c) => {
router.use('*', cors());

router.use('*', async (c, next) => {
const middleware = bearerAuth({ token: 'SECRET' });
const middleware = bearerAuth({ token: c.env.TURBO_TOKEN });
await middleware(c, next);
});

router.post('/artifacts/manual-cache-bust', zValidator('json', z.object({ expireInHours: z.number().optional() })), async (c) => {
const { expireInHours } = c.req.valid('json');
router.post('/artifacts/manual-cache-bust', async (c) => {
/**
* manual cache busting, if no expiration hours are provided, it will bust the entire cache.
* manual cache busting, it will bust the entire cache.
*/
await bustOldCache({
...c.env,
EXPIRATION_HOURS: expireInHours ?? 0,
});

// maybe this could return the keys that were busted?
return c.json({ success: true });
});

Expand Down Expand Up @@ -73,7 +79,7 @@ router.get('/v8/artifacts/:artifactID', zValidator('param', paramValidator), zVa

if (artifactID === 'list') {
const list = await c.env.R2_ARTIFACT_ARCHIVE.list();
return c.json(list.objects.map((object) => object));
return c.json(list.objects);
}

const r2Object = await c.env.R2_ARTIFACT_ARCHIVE.get(`${teamID}/${artifactID}`);
Expand Down
11 changes: 2 additions & 9 deletions wrangler.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
{
"main": "src/index.ts",
"compatibility_date": "2023-08-14",
"name": "r2-archive",
"vars": {
"EXPIRATION_HOURS": 144
},

"name": "turbo-r2-archive",
"r2_buckets": [
{
"binding": "R2_ARTIFACT_ARCHIVE",
"bucket_name": "turbo-cache",
"preview_bucket_name": "turbo-cache-preview"
}
],
"triggers": {
"crons": ["0 17 * * sun"]
}
]
}

0 comments on commit ed26ce2

Please sign in to comment.