mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
258 lines
7.5 KiB
TypeScript
258 lines
7.5 KiB
TypeScript
import { DurableObject } from "cloudflare:workers";
|
|
import { App, type CreateAppConfig } from "bknd";
|
|
import { Hono } from "hono";
|
|
import { serveStatic } from "hono/cloudflare-workers";
|
|
import type { BkndConfig, CfBkndModeCache } from "../index";
|
|
|
|
type Context = {
|
|
request: Request;
|
|
env: any;
|
|
ctx: ExecutionContext;
|
|
manifest: any;
|
|
html?: string;
|
|
};
|
|
|
|
export function serve(_config: BkndConfig, manifest?: string, html?: string) {
|
|
return {
|
|
async fetch(request: Request, env: any, ctx: ExecutionContext) {
|
|
const url = new URL(request.url);
|
|
|
|
if (manifest) {
|
|
const pathname = url.pathname.slice(1);
|
|
const assetManifest = JSON.parse(manifest);
|
|
if (pathname && pathname in assetManifest) {
|
|
const hono = new Hono();
|
|
|
|
hono.all("*", async (c, next) => {
|
|
const res = await serveStatic({
|
|
path: `./${pathname}`,
|
|
manifest,
|
|
onNotFound: (path) => console.log("not found", path)
|
|
})(c as any, next);
|
|
if (res instanceof Response) {
|
|
const ttl = pathname.startsWith("assets/")
|
|
? 60 * 60 * 24 * 365 // 1 year
|
|
: 60 * 5; // 5 minutes
|
|
res.headers.set("Cache-Control", `public, max-age=${ttl}`);
|
|
return res;
|
|
}
|
|
|
|
return c.notFound();
|
|
});
|
|
|
|
return hono.fetch(request, env);
|
|
}
|
|
}
|
|
|
|
const config = {
|
|
..._config,
|
|
setAdminHtml: _config.setAdminHtml ?? !!manifest
|
|
};
|
|
const context = { request, env, ctx, manifest, html };
|
|
const mode = config.cloudflare?.mode?.(env);
|
|
|
|
if (!mode) {
|
|
console.log("serving fresh...");
|
|
const app = await getFresh(config, context);
|
|
return app.fetch(request, env);
|
|
} else if ("cache" in mode) {
|
|
console.log("serving cached...");
|
|
const app = await getCached(config as any, context);
|
|
return app.fetch(request, env);
|
|
} else if ("durableObject" in mode) {
|
|
console.log("serving durable...");
|
|
|
|
if (config.onBuilt) {
|
|
console.log("onBuilt() is not supported with DurableObject mode");
|
|
}
|
|
|
|
const start = performance.now();
|
|
|
|
const durable = mode.durableObject;
|
|
const id = durable.idFromName(mode.key);
|
|
const stub = durable.get(id) as unknown as DurableBkndApp;
|
|
|
|
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
|
|
|
const res = await stub.fire(request, {
|
|
config: create_config,
|
|
html,
|
|
keepAliveSeconds: mode.keepAliveSeconds,
|
|
setAdminHtml: config.setAdminHtml
|
|
});
|
|
|
|
const headers = new Headers(res.headers);
|
|
headers.set("X-TTDO", String(performance.now() - start));
|
|
|
|
return new Response(res.body, {
|
|
status: res.status,
|
|
statusText: res.statusText,
|
|
headers
|
|
});
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
async function getFresh(config: BkndConfig, { env, html }: Context) {
|
|
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
|
const app = App.create(create_config);
|
|
|
|
if (config.onBuilt) {
|
|
app.emgr.onEvent(
|
|
App.Events.AppBuiltEvent,
|
|
async ({ params: { app } }) => {
|
|
config.onBuilt!(app);
|
|
},
|
|
"sync"
|
|
);
|
|
}
|
|
await app.build();
|
|
|
|
if (config.setAdminHtml) {
|
|
app.registerAdminController({ html });
|
|
}
|
|
|
|
return app;
|
|
}
|
|
|
|
async function getCached(
|
|
config: BkndConfig & { cloudflare: { mode: CfBkndModeCache } },
|
|
{ env, html, ctx }: Context
|
|
) {
|
|
const { cache, key } = config.cloudflare.mode(env) as ReturnType<CfBkndModeCache>;
|
|
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
|
|
|
const cachedConfig = await cache.get(key);
|
|
const initialConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined;
|
|
|
|
const app = App.create({ ...create_config, initialConfig });
|
|
|
|
async function saveConfig(__config: any) {
|
|
ctx.waitUntil(cache.put(key, JSON.stringify(__config)));
|
|
}
|
|
|
|
if (config.onBuilt) {
|
|
app.emgr.onEvent(
|
|
App.Events.AppBuiltEvent,
|
|
async ({ params: { app } }) => {
|
|
app.module.server.client.get("/__bknd/cache", async (c) => {
|
|
await cache.delete(key);
|
|
return c.json({ message: "Cache cleared" });
|
|
});
|
|
app.registerAdminController({ html });
|
|
|
|
config.onBuilt!(app);
|
|
},
|
|
"sync"
|
|
);
|
|
}
|
|
|
|
app.emgr.onEvent(
|
|
App.Events.AppConfigUpdatedEvent,
|
|
async ({ params: { app } }) => {
|
|
saveConfig(app.toJSON(true));
|
|
},
|
|
"sync"
|
|
);
|
|
|
|
await app.build();
|
|
|
|
if (config.setAdminHtml) {
|
|
app.registerAdminController({ html });
|
|
}
|
|
|
|
if (!cachedConfig) {
|
|
saveConfig(app.toJSON(true));
|
|
}
|
|
|
|
return app;
|
|
}
|
|
|
|
export class DurableBkndApp extends DurableObject {
|
|
protected id = Math.random().toString(36).slice(2);
|
|
protected app?: App;
|
|
protected interval?: any;
|
|
|
|
async fire(
|
|
request: Request,
|
|
options: {
|
|
config: CreateAppConfig;
|
|
html?: string;
|
|
keepAliveSeconds?: number;
|
|
setAdminHtml?: boolean;
|
|
}
|
|
) {
|
|
let buildtime = 0;
|
|
if (!this.app) {
|
|
const start = performance.now();
|
|
const config = options.config;
|
|
|
|
// change protocol to websocket if libsql
|
|
if ("type" in config.connection && config.connection.type === "libsql") {
|
|
config.connection.config.protocol = "wss";
|
|
}
|
|
|
|
this.app = App.create(config);
|
|
this.app.emgr.onEvent(
|
|
App.Events.AppBuiltEvent,
|
|
async ({ params: { app } }) => {
|
|
app.modules.server.get("/__do", async (c) => {
|
|
// @ts-ignore
|
|
const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
|
|
return c.json({
|
|
id: this.id,
|
|
keepAlive: options?.keepAliveSeconds,
|
|
colo: context.colo
|
|
});
|
|
});
|
|
},
|
|
"sync"
|
|
);
|
|
|
|
await this.app.build();
|
|
|
|
buildtime = performance.now() - start;
|
|
}
|
|
|
|
if (options?.keepAliveSeconds) {
|
|
this.keepAlive(options.keepAliveSeconds);
|
|
}
|
|
|
|
console.log("id", this.id);
|
|
const res = await this.app!.fetch(request);
|
|
const headers = new Headers(res.headers);
|
|
headers.set("X-BuildTime", buildtime.toString());
|
|
headers.set("X-DO-ID", this.id);
|
|
|
|
return new Response(res.body, {
|
|
status: res.status,
|
|
statusText: res.statusText,
|
|
headers
|
|
});
|
|
}
|
|
|
|
protected keepAlive(seconds: number) {
|
|
console.log("keep alive for", seconds);
|
|
if (this.interval) {
|
|
console.log("clearing, there is a new");
|
|
clearInterval(this.interval);
|
|
}
|
|
|
|
let i = 0;
|
|
this.interval = setInterval(() => {
|
|
i += 1;
|
|
//console.log("keep-alive", i);
|
|
if (i === seconds) {
|
|
console.log("cleared");
|
|
clearInterval(this.interval);
|
|
|
|
// ping every 30 seconds
|
|
} else if (i % 30 === 0) {
|
|
console.log("ping");
|
|
this.app?.modules.ctx().connection.ping();
|
|
}
|
|
}, 1000);
|
|
}
|
|
}
|