mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
improved cloudflare worker adapter + added docs about different modes
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import { DurableObject } from "cloudflare:workers";
|
|
||||||
import { App, type CreateAppConfig } from "bknd";
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { serveStatic } from "hono/cloudflare-workers";
|
import { serveStatic } from "hono/cloudflare-workers";
|
||||||
import type { BkndConfig, CfBkndModeCache } from "../index";
|
import type { BkndConfig } from "../index";
|
||||||
|
import { getCached } from "./modes/cached";
|
||||||
|
import { getDurable } from "./modes/durable";
|
||||||
|
import { getFresh, getWarm } from "./modes/fresh";
|
||||||
|
|
||||||
type Context = {
|
export type Context = {
|
||||||
request: Request;
|
request: Request;
|
||||||
env: any;
|
env: any;
|
||||||
ctx: ExecutionContext;
|
ctx: ExecutionContext;
|
||||||
@@ -30,9 +31,7 @@ export function serve(_config: BkndConfig, manifest?: string, html?: string) {
|
|||||||
onNotFound: (path) => console.log("not found", path)
|
onNotFound: (path) => console.log("not found", path)
|
||||||
})(c as any, next);
|
})(c as any, next);
|
||||||
if (res instanceof Response) {
|
if (res instanceof Response) {
|
||||||
const ttl = pathname.startsWith("assets/")
|
const ttl = 60 * 60 * 24 * 365;
|
||||||
? 60 * 60 * 24 * 365 // 1 year
|
|
||||||
: 60 * 5; // 5 minutes
|
|
||||||
res.headers.set("Cache-Control", `public, max-age=${ttl}`);
|
res.headers.set("Cache-Control", `public, max-age=${ttl}`);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -48,214 +47,21 @@ export function serve(_config: BkndConfig, manifest?: string, html?: string) {
|
|||||||
..._config,
|
..._config,
|
||||||
setAdminHtml: _config.setAdminHtml ?? !!manifest
|
setAdminHtml: _config.setAdminHtml ?? !!manifest
|
||||||
};
|
};
|
||||||
const context = { request, env, ctx, manifest, html };
|
const context = { request, env, ctx, manifest, html } as Context;
|
||||||
const mode = config.cloudflare?.mode?.(env);
|
const mode = config.cloudflare?.mode ?? "warm";
|
||||||
|
|
||||||
if (!mode) {
|
switch (mode) {
|
||||||
console.log("serving fresh...");
|
case "fresh":
|
||||||
const app = await getFresh(config, context);
|
return await getFresh(config, context);
|
||||||
return app.fetch(request, env);
|
case "warm":
|
||||||
} else if ("cache" in mode) {
|
return await getWarm(config, context);
|
||||||
console.log("serving cached...");
|
case "cache":
|
||||||
const app = await getCached(config as any, context);
|
return await getCached(config, context);
|
||||||
return app.fetch(request, env);
|
case "durable":
|
||||||
} else if ("durableObject" in mode) {
|
return await getDurable(config, context);
|
||||||
console.log("serving durable...");
|
default:
|
||||||
|
throw new Error(`Unknown mode ${mode}`);
|
||||||
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 (
|
|
||||||
config?.connection &&
|
|
||||||
"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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
export * from "./cloudflare-workers.adapter";
|
export * from "./cloudflare-workers.adapter";
|
||||||
|
export { makeApp, getFresh, getWarm } from "./modes/fresh";
|
||||||
|
export { getCached } from "./modes/cached";
|
||||||
|
export { DurableBkndApp, getDurable } from "./modes/durable";
|
||||||
|
|||||||
55
app/src/adapter/cloudflare/modes/cached.ts
Normal file
55
app/src/adapter/cloudflare/modes/cached.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { BkndConfig } from "adapter";
|
||||||
|
import { App } from "bknd";
|
||||||
|
import type { Context } from "../index";
|
||||||
|
|
||||||
|
export async function getCached(config: BkndConfig, { env, html, ctx }: Context) {
|
||||||
|
const { kv } = config.cloudflare?.bindings?.(env)!;
|
||||||
|
if (!kv) throw new Error("kv namespace is not defined in cloudflare.bindings");
|
||||||
|
const key = config.cloudflare?.key ?? "app";
|
||||||
|
|
||||||
|
const create_config = typeof config.app === "function" ? config.app(env) : config.app;
|
||||||
|
const cachedConfig = await kv.get(key);
|
||||||
|
const initialConfig = cachedConfig ? JSON.parse(cachedConfig) : undefined;
|
||||||
|
|
||||||
|
const app = App.create({ ...create_config, initialConfig });
|
||||||
|
|
||||||
|
async function saveConfig(__config: any) {
|
||||||
|
ctx.waitUntil(kv!.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 kv.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;
|
||||||
|
}
|
||||||
136
app/src/adapter/cloudflare/modes/durable.ts
Normal file
136
app/src/adapter/cloudflare/modes/durable.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { DurableObject } from "cloudflare:workers";
|
||||||
|
import type { BkndConfig } from "adapter";
|
||||||
|
import type { Context } from "adapter/cloudflare";
|
||||||
|
import { App, type CreateAppConfig } from "bknd";
|
||||||
|
|
||||||
|
export async function getDurable(config: BkndConfig, ctx: Context) {
|
||||||
|
const { dobj } = config.cloudflare?.bindings?.(ctx.env)!;
|
||||||
|
if (!dobj) throw new Error("durable object is not defined in cloudflare.bindings");
|
||||||
|
const key = config.cloudflare?.key ?? "app";
|
||||||
|
|
||||||
|
if (config.onBuilt) {
|
||||||
|
console.log("onBuilt() is not supported with DurableObject mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
const id = dobj.idFromName(key);
|
||||||
|
const stub = dobj.get(id) as unknown as DurableBkndApp;
|
||||||
|
|
||||||
|
const create_config = typeof config.app === "function" ? config.app(ctx.env) : config.app;
|
||||||
|
|
||||||
|
const res = await stub.fire(ctx.request, {
|
||||||
|
config: create_config,
|
||||||
|
html: ctx.html,
|
||||||
|
keepAliveSeconds: config.cloudflare?.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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
config?.connection &&
|
||||||
|
"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,
|
||||||
|
keepAliveSeconds: options?.keepAliveSeconds ?? 0,
|
||||||
|
colo: context.colo
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.onBuilt(app);
|
||||||
|
},
|
||||||
|
"sync"
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.app.build();
|
||||||
|
|
||||||
|
if (options.setAdminHtml) {
|
||||||
|
this.app.registerAdminController({ html: options.html });
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBuilt(app: App) {}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/src/adapter/cloudflare/modes/fresh.ts
Normal file
39
app/src/adapter/cloudflare/modes/fresh.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { BkndConfig } from "adapter";
|
||||||
|
import { App } from "bknd";
|
||||||
|
import type { Context } from "../index";
|
||||||
|
|
||||||
|
export async function makeApp(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFresh(config: BkndConfig, ctx: Context) {
|
||||||
|
const app = await makeApp(config, ctx);
|
||||||
|
return app.fetch(ctx.request);
|
||||||
|
}
|
||||||
|
|
||||||
|
let warm_app: App;
|
||||||
|
export async function getWarm(config: BkndConfig, ctx: Context) {
|
||||||
|
if (!warm_app) {
|
||||||
|
warm_app = await makeApp(config, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return warm_app.fetch(ctx.request);
|
||||||
|
}
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
import type { IncomingMessage } from "node:http";
|
import type { IncomingMessage } from "node:http";
|
||||||
import type { App, CreateAppConfig } from "bknd";
|
import type { App, CreateAppConfig } from "bknd";
|
||||||
|
|
||||||
export type CfBkndModeCache<Env = any> = (env: Env) => {
|
|
||||||
cache: KVNamespace;
|
|
||||||
key: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CfBkndModeDurableObject<Env = any> = (env: Env) => {
|
|
||||||
durableObject: DurableObjectNamespace;
|
|
||||||
key: string;
|
|
||||||
keepAliveSeconds?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CloudflareBkndConfig<Env = any> = {
|
export type CloudflareBkndConfig<Env = any> = {
|
||||||
mode?: CfBkndModeCache | CfBkndModeDurableObject;
|
mode?: "warm" | "fresh" | "cache" | "durable";
|
||||||
|
bindings?: (env: Env) => {
|
||||||
|
kv?: KVNamespace;
|
||||||
|
dobj?: DurableObjectNamespace;
|
||||||
|
};
|
||||||
|
key?: string;
|
||||||
|
keepAliveSeconds?: number;
|
||||||
forceHttps?: boolean;
|
forceHttps?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,14 +24,6 @@ export type BkndConfig<Env = any> = {
|
|||||||
onBuilt?: (app: App) => Promise<void>;
|
onBuilt?: (app: App) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BkndConfigJson = {
|
|
||||||
app: CreateAppConfig;
|
|
||||||
setAdminHtml?: boolean;
|
|
||||||
server?: {
|
|
||||||
port?: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function nodeRequestToRequest(req: IncomingMessage): Request {
|
export function nodeRequestToRequest(req: IncomingMessage): Request {
|
||||||
let protocol = "http";
|
let protocol = "http";
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -20,11 +20,16 @@ export class DebugLogger {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.last = 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
log(...args: any[]) {
|
log(...args: any[]) {
|
||||||
if (!this._enabled) return this;
|
if (!this._enabled) return this;
|
||||||
|
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const time = Number.parseInt(String(now - this.last));
|
const time = this.last === 0 ? 0 : Number.parseInt(String(now - this.last));
|
||||||
const indents = " ".repeat(this._context.length);
|
const indents = " ".repeat(this._context.length);
|
||||||
const context =
|
const context =
|
||||||
this._context.length > 0 ? `[${this._context[this._context.length - 1]}]` : "";
|
this._context.length > 0 ? `[${this._context[this._context.length - 1]}]` : "";
|
||||||
|
|||||||
@@ -13,22 +13,23 @@ and then install bknd as a dependency:
|
|||||||
|
|
||||||
|
|
||||||
## Serve the API
|
## Serve the API
|
||||||
|
If you don't choose anything specific, the following code will use the `warm` mode. See the
|
||||||
|
chapter [Using a different mode](#using-a-different-mode) for available modes.
|
||||||
|
|
||||||
``` ts
|
``` ts
|
||||||
import { serve } from "bknd/adapter/cloudflare";
|
import { serve } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
export default serve(
|
export default serve({
|
||||||
{
|
app: (env: Env) => ({
|
||||||
app: (env: Env) => ({
|
connection: {
|
||||||
connection: {
|
type: "libsql",
|
||||||
type: "libsql",
|
config: {
|
||||||
config: {
|
url: env.DB_URL,
|
||||||
url: env.DB_URL,
|
authToken: env.DB_TOKEN
|
||||||
authToken: env.DB_TOKEN
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
);
|
});
|
||||||
```
|
```
|
||||||
For more information about the connection object, refer to the [Setup](/setup) guide.
|
For more information about the connection object, refer to the [Setup](/setup) guide.
|
||||||
|
|
||||||
@@ -49,49 +50,152 @@ bucket = "node_modules/bknd/dist/static"
|
|||||||
```
|
```
|
||||||
|
|
||||||
And then modify the worker entry as follows:
|
And then modify the worker entry as follows:
|
||||||
``` ts {2, 15, 17}
|
``` ts {2, 14, 15}
|
||||||
import { serve } from "bknd/adapter/cloudflare";
|
import { serve } from "bknd/adapter/cloudflare";
|
||||||
import manifest from "__STATIC_CONTENT_MANIFEST";
|
import manifest from "__STATIC_CONTENT_MANIFEST";
|
||||||
|
|
||||||
export default serve(
|
export default serve({
|
||||||
{
|
app: (env: Env) => ({
|
||||||
app: (env: Env) => ({
|
connection: {
|
||||||
connection: {
|
type: "libsql",
|
||||||
type: "libsql",
|
config: {
|
||||||
config: {
|
url: env.DB_URL,
|
||||||
url: env.DB_URL,
|
authToken: env.DB_TOKEN
|
||||||
authToken: env.DB_TOKEN
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
setAdminHtml: true
|
}),
|
||||||
},
|
setAdminHtml: true
|
||||||
manifest
|
}, manifest);
|
||||||
);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Adding custom routes
|
## Adding custom routes
|
||||||
You can also add custom routes by defining them after the app has been built, like so:
|
You can also add custom routes by defining them after the app has been built, like so:
|
||||||
```ts {15-17}
|
```ts {14-16}
|
||||||
import { serve } from "bknd/adapter/cloudflare";
|
import { serve } from "bknd/adapter/cloudflare";
|
||||||
import manifest from "__STATIC_CONTENT_MANIFEST";
|
import manifest from "__STATIC_CONTENT_MANIFEST";
|
||||||
|
|
||||||
export default serve(
|
export default serve({
|
||||||
{
|
app: (env: Env) => ({
|
||||||
app: (env: Env) => ({
|
connection: {
|
||||||
connection: {
|
type: "libsql",
|
||||||
type: "libsql",
|
config: {
|
||||||
config: {
|
url: env.DB_URL,
|
||||||
url: env.DB_URL,
|
authToken: env.DB_TOKEN
|
||||||
authToken: env.DB_TOKEN
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
onBuilt: async (app) => {
|
}),
|
||||||
app.modules.server.get("/hello", (c) => c.json({ hello: "world" }));
|
onBuilt: async (app) => {
|
||||||
},
|
app.modules.server.get("/hello", (c) => c.json({ hello: "world" }));
|
||||||
setAdminHtml: true
|
|
||||||
},
|
},
|
||||||
manifest
|
setAdminHtml: true
|
||||||
);
|
}, manifest);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using a different mode
|
||||||
|
With the Cloudflare Workers adapter, you're being offered to 4 modes to choose from (default:
|
||||||
|
`warm`):
|
||||||
|
|
||||||
|
| Mode | Description | Use Case |
|
||||||
|
|:----------|:-------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `fresh` | On every request, the configuration gets refetched, app built and then served. | Ideal if you don't want to deal with eviction, KV or Durable Objects. |
|
||||||
|
| `warm` | It tries to keep the built app in memory for as long as possible, and rebuilds if evicted. | Better response times, should be the default choice. |
|
||||||
|
| `cache` | The configuration is fetched from KV to reduce the initial roundtrip to the database. | Generally faster response times with irregular access patterns. |
|
||||||
|
| `durable` | The bknd app is ran inside a Durable Object and can be configured to stay alive. | Slowest boot time, but fastest responses. Can be kept alive for as long as you want, giving similar response times as server instances. |
|
||||||
|
|
||||||
|
### Modes: `fresh` and `warm`
|
||||||
|
To use either `fresh` or `warm`, all you have to do is adding the desired mode to `cloudflare.
|
||||||
|
mode`, like so:
|
||||||
|
```ts
|
||||||
|
import { serve } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
|
export default serve({
|
||||||
|
app: (env: Env) => ({ /* ... */ }),
|
||||||
|
cloudflare: {
|
||||||
|
mode: "fresh"
|
||||||
|
// mode: "warm"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mode: `cache`
|
||||||
|
For the cache mode to work, you also need to specify the KV to be used. For this, use the
|
||||||
|
`bindings` property:
|
||||||
|
```ts
|
||||||
|
import { serve } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
|
export default serve({
|
||||||
|
app: (env: Env) => ({ /* ... */ }),
|
||||||
|
cloudflare: {
|
||||||
|
mode: "cache",
|
||||||
|
bindings: (env: Env) => ({ kv: env.KV })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mode: `durable` (advanced)
|
||||||
|
To use the `durable` mode, you have to specify the Durable Object to extract from your
|
||||||
|
environment, and additionally export the `DurableBkndApp` class:
|
||||||
|
```ts
|
||||||
|
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
|
export { DurableBkndApp };
|
||||||
|
export default serve({
|
||||||
|
app: (env: Env) => ({ /* ... */ }),
|
||||||
|
cloudflare: {
|
||||||
|
mode: "durable",
|
||||||
|
bindings: (env: Env) => ({ dobj: env.DOBJ }),
|
||||||
|
keepAliveSeconds: 60 // optional
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, you need to define the Durable Object in your `wrangler.toml` file (refer to the [Durable
|
||||||
|
Objects](https://developers.cloudflare.com/durable-objects/) documentation):
|
||||||
|
```toml
|
||||||
|
[[durable_objects.bindings]]
|
||||||
|
name = "DOBJ"
|
||||||
|
class_name = "DurableBkndApp"
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v1"
|
||||||
|
new_classes = ["DurableBkndApp"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Since the communication between the Worker and Durable Object is serialized, the `onBuilt`
|
||||||
|
property won't work. To use it (e.g. to specify special routes), you need to extend from the
|
||||||
|
`DurableBkndApp`:
|
||||||
|
```ts
|
||||||
|
import type { App } from "bknd";
|
||||||
|
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
|
||||||
|
|
||||||
|
export default serve({
|
||||||
|
app: (env: Env) => ({ /* ... */ }),
|
||||||
|
cloudflare: {
|
||||||
|
mode: "durable",
|
||||||
|
bindings: (env: Env) => ({ dobj: env.DOBJ }),
|
||||||
|
keepAliveSeconds: 60 // optional
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export class CustomDurableBkndApp extends DurableBkndApp {
|
||||||
|
async onBuilt(app: App) {
|
||||||
|
app.modules.server.get("/custom/endpoint", (c) => c.text("Custom"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
In case you've already deployed your Worker, the deploy command may complain about a new class
|
||||||
|
being used. To fix this issue, you need to add a "rename migration":
|
||||||
|
```toml
|
||||||
|
[[durable_objects.bindings]]
|
||||||
|
name = "DOBJ"
|
||||||
|
class_name = "CustomDurableBkndApp"
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v1"
|
||||||
|
new_classes = ["DurableBkndApp"]
|
||||||
|
|
||||||
|
[[migrations]]
|
||||||
|
tag = "v2"
|
||||||
|
renamed_classes = [{from = "DurableBkndApp", to = "CustomDurableBkndApp"}]
|
||||||
|
deleted_classes = ["DurableBkndApp"]
|
||||||
```
|
```
|
||||||
Reference in New Issue
Block a user