Skip to content

Commit

Permalink
Simple cron program (#15)
Browse files Browse the repository at this point in the history
* Create main.ts

* wip `cf:deploy` task

* add `cloudflare/workers/daily.ts`

* bump version

* add `cf:push`, `cf:serve` task

Resolves <#15 (comment)>.

* rename `/cf`

* wip

* fix denoflare

### Inspiration

[Discord conversation](https://discord.com/channels/684898665143206084/1147601703131152494/1147905102582132869):
> [7:45 AM]John Spurlock: just define a scheduled handler alongside your module fetch handler like any other worker.  Full signature looks like:
> ```ts
> scheduled(event: ModuleWorkerScheduledEvent, env: MyWorkerEnv, ctx: ModuleWorkerContext): Promise<void>;
> ```
> [Image](https://cdn.discordapp.com/attachments/1147601703131152494/1147905102334664704/image.png)
> [7:47 AM]John Spurlock: there's no way to simulate cron events locally - for my workers I just call into common code that's also reachable by an admin endpoint from fetch for easy testing

### Results

So what I ended up doing was following John Spurlock's advice in just defining `scheduled` as an adjacent method to the required `fetch` method.

* Update .env.example

* aesthetic changes
  • Loading branch information
EthanThatOneKid authored Sep 4, 2023
1 parent 98a2e69 commit a8b956b
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

.env
ngrok.exe
.denoflare
69 changes: 69 additions & 0 deletions cf/dailies/dailies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* ScheduledEvent is the expected Cloudflare event for this worker.
*/
export interface ScheduledEvent {
cron: string;
}

/**
* Env is the expected environment variables for this worker.
*/
export interface Env {
WEBHOOK_URL: string;
}

/**
* Ctx is the expected context for this worker.
*/
interface Ctx {
waitUntil(promise: Promise<unknown>): void;
}

export default {
/**
* fetch is executed on every request.
*/
async fetch(request: Request, env: Env) {
if (request.method !== "GET") {
return new Response("Method not allowed", { status: 405 });
}

const url = new URL(request.url);
if (url.pathname !== "/__scheduled") {
return new Response("Not found", { status: 404 });
}

const cron = url.searchParams.get("cron");
if (cron !== CRON_EXPRESSION) {
return new Response("Unexpected cron expression", { status: 400 });
}

return await execute(env.WEBHOOK_URL);
},

/**
* scheduled is executed daily at 12:00 AM UTC.
*
* See:
* - <https://developers.cloudflare.com/workers/runtime-apis/scheduled-event/>
*/
scheduled(event: ScheduledEvent, env: Env, ctx: Ctx) {
if (event.cron !== CRON_EXPRESSION) {
return;
}

ctx.waitUntil(execute(env.WEBHOOK_URL));
},
};

function execute(webhookURL: string) {
return fetch(webhookURL, { method: "POST" });
}

/**
* CRON_EXPRESSION is the cron expression for the scheduled event.
*
* See:
* - <https://crontab.guru/#0_0_*_*_*>
*/
const CRON_EXPRESSION = "0 0 * * *";
42 changes: 42 additions & 0 deletions cf/dailies/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { load } from "lc-dailies/deps.ts";
import {
denoflare,
DENOFLARE_VERSION_TAG,
} from "lc-dailies/lib/denoflare/mod.ts";

await load({ export: true });

const CF_ACCOUNT_ID = Deno.env.get("CF_ACCOUNT_ID")!;
const CF_API_TOKEN = Deno.env.get("CF_API_TOKEN")!;
const WEBHOOK_URL = Deno.env.get("WEBHOOK_URL")!;

const DENOFLARE_SCRIPT_NAME = "lc-dailies";
const DENOFLARE_SCRIPT_SPECIFIER = "cf/dailies/dailies.ts";

async function daily(...args: string[]) {
return await denoflare({
versionTag: DENOFLARE_VERSION_TAG,
scriptName: DENOFLARE_SCRIPT_NAME,
path: DENOFLARE_SCRIPT_SPECIFIER,
cfAccountID: CF_ACCOUNT_ID,
cfAPIToken: CF_API_TOKEN,
localPort: 8080,
args,
});
}

export async function serve() {
return await daily(
"serve",
DENOFLARE_SCRIPT_NAME,
"--secret-binding",
`WEBHOOK_URL:${WEBHOOK_URL}`,
);
}

export async function push() {
return await daily(
"push",
DENOFLARE_SCRIPT_NAME,
);
}
5 changes: 5 additions & 0 deletions cf/dailies/push/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { push } from "../env.ts";

if (import.meta.main) {
await push();
}
5 changes: 5 additions & 0 deletions cf/dailies/serve/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { serve } from "../env.ts";

if (import.meta.main) {
await serve();
}
4 changes: 3 additions & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"all": "deno task udd && deno lint && deno fmt",
"test": "deno test --unstable",
"start": "deno run -A --unstable main.ts",
"ngrok": "ngrok http 8080"
"ngrok": "ngrok http 8080",
"cf:push": "deno run -A cf/dailies/push/main.ts",
"cf:serve": "deno run -A cf/dailies/serve/main.ts"
},
"imports": {
"lc-dailies/": "./"
Expand Down
68 changes: 68 additions & 0 deletions lib/denoflare/denoflare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const DENOFLARE_CONFIG_FILENAME = ".denoflare";

/**
* DENOFLARE_VERSION_TAG is the version tag of denoflare.
*/
export const DENOFLARE_VERSION_TAG = "v0.5.12";

/**
* DenoflareOptions is the options for denoflare.
*/
export interface DenoflareOptions {
versionTag: string;
scriptName: string;
path: string;
cfAccountID: string;
cfAPIToken: string;
localPort: number;
args: string[];
}

/**
* denoflare is a helper for interfacing with the denoflare CLI.
*
* See: https://denoflare.dev/cli/
*/
export async function denoflare(options: DenoflareOptions) {
const moduleURL = `https://deno.land/x/denoflare@${options.versionTag}`;
const config = {
$schema: `${moduleURL}/common/config.schema.json`,
scripts: {
[options.scriptName]: {
path: options.path,
localPort: options.localPort,
},
},
profiles: {
profile: {
accountId: options.cfAccountID,
apiToken: options.cfAPIToken,
},
},
};
await Deno.writeTextFile(
DENOFLARE_CONFIG_FILENAME,
JSON.stringify(config),
);

try {
// Create a child process running denoflare CLI.
const child = new Deno.Command(Deno.execPath(), {
args: [
"run",
"-A",
"--unstable",
`${moduleURL}/cli/cli.ts`,
...options.args,
],
stdin: "piped",
stdout: "piped",
}).spawn();

// Pipe the child process stdout to stdout.
await child.stdout.pipeTo(Deno.stdout.writable);
} finally {
// Delete the temporary config file.
await Deno.remove(DENOFLARE_CONFIG_FILENAME);
}
}
1 change: 1 addition & 0 deletions lib/denoflare/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./denoflare.ts";

0 comments on commit a8b956b

Please sign in to comment.