mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
refactored EventManager to run asyncs on call only, app defaults to run before response (#129)
* refactored EventManager to run asyncs on call only, app defaults to run before response * fix tests
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, mock, test } from "bun:test";
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
import type { ModuleBuildContext } from "../../src";
|
import type { ModuleBuildContext } from "../../src";
|
||||||
import { type App, createApp } from "../../src/App";
|
import { App, createApp } from "../../src/App";
|
||||||
import * as proto from "../../src/data/prototype";
|
import * as proto from "../../src/data/prototype";
|
||||||
|
|
||||||
describe("App", () => {
|
describe("App", () => {
|
||||||
@@ -51,4 +51,87 @@ describe("App", () => {
|
|||||||
expect(todos[0]?.title).toBe("ctx");
|
expect(todos[0]?.title).toBe("ctx");
|
||||||
expect(todos[1]?.title).toBe("api");
|
expect(todos[1]?.title).toBe("api");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("lifecycle events are triggered", async () => {
|
||||||
|
const firstBoot = mock(() => null);
|
||||||
|
const configUpdate = mock(() => null);
|
||||||
|
const appBuilt = mock(() => null);
|
||||||
|
const appRequest = mock(() => null);
|
||||||
|
const beforeResponse = mock(() => null);
|
||||||
|
|
||||||
|
const app = createApp();
|
||||||
|
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppFirstBoot,
|
||||||
|
(event) => {
|
||||||
|
expect(event).toBeInstanceOf(App.Events.AppFirstBoot);
|
||||||
|
expect(event.params.app.version()).toBe(app.version());
|
||||||
|
firstBoot();
|
||||||
|
},
|
||||||
|
"sync",
|
||||||
|
);
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppBuiltEvent,
|
||||||
|
(event) => {
|
||||||
|
expect(event).toBeInstanceOf(App.Events.AppBuiltEvent);
|
||||||
|
expect(event.params.app.version()).toBe(app.version());
|
||||||
|
appBuilt();
|
||||||
|
},
|
||||||
|
"sync",
|
||||||
|
);
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppConfigUpdatedEvent,
|
||||||
|
() => {
|
||||||
|
configUpdate();
|
||||||
|
},
|
||||||
|
"sync",
|
||||||
|
);
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppRequest,
|
||||||
|
(event) => {
|
||||||
|
expect(event).toBeInstanceOf(App.Events.AppRequest);
|
||||||
|
expect(event.params.app.version()).toBe(app.version());
|
||||||
|
expect(event.params.request).toBeInstanceOf(Request);
|
||||||
|
appRequest();
|
||||||
|
},
|
||||||
|
"sync",
|
||||||
|
);
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppBeforeResponse,
|
||||||
|
(event) => {
|
||||||
|
expect(event).toBeInstanceOf(App.Events.AppBeforeResponse);
|
||||||
|
expect(event.params.app.version()).toBe(app.version());
|
||||||
|
expect(event.params.response).toBeInstanceOf(Response);
|
||||||
|
beforeResponse();
|
||||||
|
},
|
||||||
|
"sync",
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.build();
|
||||||
|
expect(firstBoot).toHaveBeenCalled();
|
||||||
|
expect(appBuilt).toHaveBeenCalled();
|
||||||
|
//expect(configUpdate).toHaveBeenCalled();
|
||||||
|
expect(appRequest).not.toHaveBeenCalled();
|
||||||
|
expect(beforeResponse).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emgr exec modes", async () => {
|
||||||
|
const called = mock(() => null);
|
||||||
|
const app = createApp({
|
||||||
|
options: {
|
||||||
|
asyncEventsMode: "sync",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// register async listener
|
||||||
|
app.emgr.onEvent(App.Events.AppFirstBoot, async () => {
|
||||||
|
called();
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.build();
|
||||||
|
await app.server.request(new Request("http://localhost"));
|
||||||
|
|
||||||
|
// expect async listeners to be executed sync after request
|
||||||
|
expect(called).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ describe("EventManager", async () => {
|
|||||||
new SpecialEvent({ foo: "bar" });
|
new SpecialEvent({ foo: "bar" });
|
||||||
new InformationalEvent();
|
new InformationalEvent();
|
||||||
|
|
||||||
|
// execute asyncs
|
||||||
|
await emgr.executeAsyncs();
|
||||||
|
|
||||||
expect(call).toHaveBeenCalledTimes(2);
|
expect(call).toHaveBeenCalledTimes(2);
|
||||||
expect(delayed).toHaveBeenCalled();
|
expect(delayed).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -80,15 +83,11 @@ describe("EventManager", async () => {
|
|||||||
call();
|
call();
|
||||||
return Promise.all(p);
|
return Promise.all(p);
|
||||||
};
|
};
|
||||||
const emgr = new EventManager(
|
const emgr = new EventManager({ InformationalEvent });
|
||||||
{ InformationalEvent },
|
|
||||||
{
|
|
||||||
asyncExecutor,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
emgr.onEvent(InformationalEvent, async () => {});
|
emgr.onEvent(InformationalEvent, async () => {});
|
||||||
await emgr.emit(new InformationalEvent());
|
await emgr.emit(new InformationalEvent());
|
||||||
|
await emgr.executeAsyncs(asyncExecutor);
|
||||||
expect(call).toHaveBeenCalled();
|
expect(call).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,6 +124,9 @@ describe("EventManager", async () => {
|
|||||||
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
|
const e2 = await emgr.emit(new ReturnEvent({ foo: "bar" }));
|
||||||
expect(e2.returned).toBe(true);
|
expect(e2.returned).toBe(true);
|
||||||
expect(e2.params.foo).toBe("bar-1-0");
|
expect(e2.params.foo).toBe("bar-1-0");
|
||||||
|
|
||||||
|
await emgr.executeAsyncs();
|
||||||
|
|
||||||
expect(onInvalidReturn).toHaveBeenCalled();
|
expect(onInvalidReturn).toHaveBeenCalled();
|
||||||
expect(asyncEventCallback).toHaveBeenCalled();
|
expect(asyncEventCallback).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -288,14 +288,17 @@ describe("[data] Mutator (Events)", async () => {
|
|||||||
|
|
||||||
test("events were fired", async () => {
|
test("events were fired", async () => {
|
||||||
const { data } = await mutator.insertOne({ label: "test" });
|
const { data } = await mutator.insertOne({ label: "test" });
|
||||||
|
await mutator.emgr.executeAsyncs();
|
||||||
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorInsertBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorInsertAfter.slug)).toBeTrue();
|
||||||
|
|
||||||
await mutator.updateOne(data.id, { label: "test2" });
|
await mutator.updateOne(data.id, { label: "test2" });
|
||||||
|
await mutator.emgr.executeAsyncs();
|
||||||
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorUpdateBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorUpdateAfter.slug)).toBeTrue();
|
||||||
|
|
||||||
await mutator.deleteOne(data.id);
|
await mutator.deleteOne(data.id);
|
||||||
|
await mutator.emgr.executeAsyncs();
|
||||||
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorDeleteBefore.slug)).toBeTrue();
|
||||||
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
expect(events.has(MutatorEvents.MutatorDeleteAfter.slug)).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -198,22 +198,27 @@ describe("[data] Repository (Events)", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("events were fired", async () => {
|
test("events were fired", async () => {
|
||||||
await em.repository(items).findId(1);
|
const repo = em.repository(items);
|
||||||
|
await repo.findId(1);
|
||||||
|
await repo.emgr.executeAsyncs();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
await em.repository(items).findOne({ id: 1 });
|
await repo.findOne({ id: 1 });
|
||||||
|
await repo.emgr.executeAsyncs();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindOneAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
await em.repository(items).findMany({ where: { id: 1 } });
|
await repo.findMany({ where: { id: 1 } });
|
||||||
|
await repo.emgr.executeAsyncs();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
await em.repository(items).findManyByReference(1, "categories");
|
await repo.findManyByReference(1, "categories");
|
||||||
|
await repo.emgr.executeAsyncs();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyBefore.slug)).toBeTrue();
|
||||||
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
expect(events.has(RepositoryEvents.RepositoryFindManyAfter.slug)).toBeTrue();
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { type FileBody, Storage, type StorageAdapter } from "../../src/media/storage/Storage";
|
import { type FileBody, Storage } from "../../src/media/storage/Storage";
|
||||||
import * as StorageEvents from "../../src/media/storage/events";
|
import * as StorageEvents from "../../src/media/storage/events";
|
||||||
|
import { StorageAdapter } from "media";
|
||||||
|
|
||||||
class TestAdapter implements StorageAdapter {
|
class TestAdapter extends StorageAdapter {
|
||||||
files: Record<string, FileBody> = {};
|
files: Record<string, FileBody> = {};
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
@@ -61,7 +62,7 @@ describe("Storage", async () => {
|
|||||||
test("uploads a file", async () => {
|
test("uploads a file", async () => {
|
||||||
const {
|
const {
|
||||||
meta: { type, size },
|
meta: { type, size },
|
||||||
} = await storage.uploadFile("hello", "world.txt");
|
} = await storage.uploadFile("hello" as any, "world.txt");
|
||||||
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
expect({ type, size }).toEqual({ type: "text/plain", size: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ describe("Storage", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("events were fired", async () => {
|
test("events were fired", async () => {
|
||||||
|
await storage.emgr.executeAsyncs();
|
||||||
expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue();
|
expect(events.has(StorageEvents.FileUploadedEvent.slug)).toBeTrue();
|
||||||
expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue();
|
expect(events.has(StorageEvents.FileDeletedEvent.slug)).toBeTrue();
|
||||||
// @todo: file access must be tested in controllers
|
// @todo: file access must be tested in controllers
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { Event } from "core/events";
|
|||||||
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import {
|
import {
|
||||||
|
ModuleManager,
|
||||||
type InitialModuleConfigs,
|
type InitialModuleConfigs,
|
||||||
type ModuleBuildContext,
|
type ModuleBuildContext,
|
||||||
ModuleManager,
|
type ModuleConfigs,
|
||||||
type ModuleManagerOptions,
|
type ModuleManagerOptions,
|
||||||
type Modules,
|
type Modules,
|
||||||
} from "modules/ModuleManager";
|
} from "modules/ModuleManager";
|
||||||
@@ -16,6 +17,7 @@ import { SystemController } from "modules/server/SystemController";
|
|||||||
|
|
||||||
// biome-ignore format: must be there
|
// biome-ignore format: must be there
|
||||||
import { Api, type ApiOptions } from "Api";
|
import { Api, type ApiOptions } from "Api";
|
||||||
|
import type { ServerEnv } from "modules/Controller";
|
||||||
|
|
||||||
export type AppPlugin = (app: App) => Promise<void> | void;
|
export type AppPlugin = (app: App) => Promise<void> | void;
|
||||||
|
|
||||||
@@ -29,12 +31,25 @@ export class AppBuiltEvent extends AppEvent {
|
|||||||
export class AppFirstBoot extends AppEvent {
|
export class AppFirstBoot extends AppEvent {
|
||||||
static override slug = "app-first-boot";
|
static override slug = "app-first-boot";
|
||||||
}
|
}
|
||||||
export const AppEvents = { AppConfigUpdatedEvent, AppBuiltEvent, AppFirstBoot } as const;
|
export class AppRequest extends AppEvent<{ request: Request }> {
|
||||||
|
static override slug = "app-request";
|
||||||
|
}
|
||||||
|
export class AppBeforeResponse extends AppEvent<{ request: Request; response: Response }> {
|
||||||
|
static override slug = "app-before-response";
|
||||||
|
}
|
||||||
|
export const AppEvents = {
|
||||||
|
AppConfigUpdatedEvent,
|
||||||
|
AppBuiltEvent,
|
||||||
|
AppFirstBoot,
|
||||||
|
AppRequest,
|
||||||
|
AppBeforeResponse,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export type AppOptions = {
|
export type AppOptions = {
|
||||||
plugins?: AppPlugin[];
|
plugins?: AppPlugin[];
|
||||||
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
seed?: (ctx: ModuleBuildContext & { app: App }) => Promise<void>;
|
||||||
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
manager?: Omit<ModuleManagerOptions, "initial" | "onUpdated" | "seed">;
|
||||||
|
asyncEventsMode?: "sync" | "async" | "none";
|
||||||
};
|
};
|
||||||
export type CreateAppConfig = {
|
export type CreateAppConfig = {
|
||||||
connection?:
|
connection?:
|
||||||
@@ -70,35 +85,9 @@ export class App {
|
|||||||
this.modules = new ModuleManager(connection, {
|
this.modules = new ModuleManager(connection, {
|
||||||
...(options?.manager ?? {}),
|
...(options?.manager ?? {}),
|
||||||
initial: _initialConfig,
|
initial: _initialConfig,
|
||||||
onUpdated: async (key, config) => {
|
onUpdated: this.onUpdated.bind(this),
|
||||||
// if the EventManager was disabled, we assume we shouldn't
|
onFirstBoot: this.onFirstBoot.bind(this),
|
||||||
// respond to events, such as "onUpdated".
|
onServerInit: this.onServerInit.bind(this),
|
||||||
// this is important if multiple changes are done, and then build() is called manually
|
|
||||||
if (!this.emgr.enabled) {
|
|
||||||
$console.warn("App config updated, but event manager is disabled, skip.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$console.log("App config updated", key);
|
|
||||||
// @todo: potentially double syncing
|
|
||||||
await this.build({ sync: true });
|
|
||||||
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
|
||||||
},
|
|
||||||
onFirstBoot: async () => {
|
|
||||||
$console.log("App first boot");
|
|
||||||
this.trigger_first_boot = true;
|
|
||||||
},
|
|
||||||
onServerInit: async (server) => {
|
|
||||||
server.use(async (c, next) => {
|
|
||||||
c.set("app", this);
|
|
||||||
await next();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// gracefully add the app id
|
|
||||||
c.res.headers.set("X-bknd-id", this._id);
|
|
||||||
} catch (e) {}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
this.modules.ctx().emgr.registerEvents(AppEvents);
|
this.modules.ctx().emgr.registerEvents(AppEvents);
|
||||||
}
|
}
|
||||||
@@ -213,6 +202,53 @@ export class App {
|
|||||||
|
|
||||||
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
return new Api({ host: "http://localhost", ...(options ?? {}), fetcher });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onUpdated<Module extends keyof Modules>(module: Module, config: ModuleConfigs[Module]) {
|
||||||
|
// if the EventManager was disabled, we assume we shouldn't
|
||||||
|
// respond to events, such as "onUpdated".
|
||||||
|
// this is important if multiple changes are done, and then build() is called manually
|
||||||
|
if (!this.emgr.enabled) {
|
||||||
|
$console.warn("App config updated, but event manager is disabled, skip.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$console.log("App config updated", module);
|
||||||
|
// @todo: potentially double syncing
|
||||||
|
await this.build({ sync: true });
|
||||||
|
await this.emgr.emit(new AppConfigUpdatedEvent({ app: this }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async onFirstBoot() {
|
||||||
|
$console.log("App first boot");
|
||||||
|
this.trigger_first_boot = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onServerInit(server: Hono<ServerEnv>) {
|
||||||
|
server.use(async (c, next) => {
|
||||||
|
c.set("app", this);
|
||||||
|
await this.emgr.emit(new AppRequest({ app: this, request: c.req.raw }));
|
||||||
|
await next();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// gracefully add the app id
|
||||||
|
c.res.headers.set("X-bknd-id", this._id);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
await this.emgr.emit(
|
||||||
|
new AppBeforeResponse({ app: this, request: c.req.raw, response: c.res }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// execute collected async events (async by default)
|
||||||
|
switch (this.options?.asyncEventsMode ?? "async") {
|
||||||
|
case "sync":
|
||||||
|
await this.emgr.executeAsyncs();
|
||||||
|
break;
|
||||||
|
case "async":
|
||||||
|
this.emgr.executeAsyncs();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createApp(config: CreateAppConfig = {}) {
|
export function createApp(config: CreateAppConfig = {}) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getBinding } from "./bindings";
|
|||||||
import { getCached } from "./modes/cached";
|
import { getCached } from "./modes/cached";
|
||||||
import { getDurable } from "./modes/durable";
|
import { getDurable } from "./modes/durable";
|
||||||
import { getFresh, getWarm } from "./modes/fresh";
|
import { getFresh, getWarm } from "./modes/fresh";
|
||||||
|
import type { CreateAppConfig } from "App";
|
||||||
|
|
||||||
export type CloudflareBkndConfig<Env = any> = FrameworkBkndConfig<Context<Env>> & {
|
export type CloudflareBkndConfig<Env = any> = FrameworkBkndConfig<Context<Env>> & {
|
||||||
mode?: "warm" | "fresh" | "cache" | "durable";
|
mode?: "warm" | "fresh" | "cache" | "durable";
|
||||||
@@ -32,8 +33,14 @@ export type Context<Env = any> = {
|
|||||||
ctx: ExecutionContext;
|
ctx: ExecutionContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const constants = {
|
||||||
|
exec_async_event_id: "cf_register_waituntil",
|
||||||
|
cache_endpoint: "/__bknd/cache",
|
||||||
|
do_endpoint: "/__bknd/do",
|
||||||
|
};
|
||||||
|
|
||||||
let media_registered: boolean = false;
|
let media_registered: boolean = false;
|
||||||
export function makeCfConfig(config: CloudflareBkndConfig, context: Context) {
|
export function makeCfConfig(config: CloudflareBkndConfig, context: Context): CreateAppConfig {
|
||||||
if (!media_registered) {
|
if (!media_registered) {
|
||||||
registerMedia(context.env as any);
|
registerMedia(context.env as any);
|
||||||
media_registered = true;
|
media_registered = true;
|
||||||
@@ -61,7 +68,14 @@ export function makeCfConfig(config: CloudflareBkndConfig, context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return appConfig;
|
return {
|
||||||
|
...appConfig,
|
||||||
|
options: {
|
||||||
|
...appConfig.options,
|
||||||
|
// if not specified explicitly, disable it to use ExecutionContext's waitUntil
|
||||||
|
asyncEventsMode: config.options?.asyncEventsMode ?? "none",
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
|
export function serve<Env = any>(config: CloudflareBkndConfig<Env> = {}) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { App } from "bknd";
|
import { App } from "bknd";
|
||||||
import { createRuntimeApp } from "bknd/adapter";
|
import { createRuntimeApp } from "bknd/adapter";
|
||||||
import { type CloudflareBkndConfig, type Context, makeCfConfig } from "../index";
|
import { type CloudflareBkndConfig, constants, type Context, makeCfConfig } from "../index";
|
||||||
|
|
||||||
export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...args }: Context) {
|
export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...args }: Context) {
|
||||||
const { kv } = config.bindings?.(env)!;
|
const { kv } = config.bindings?.(env)!;
|
||||||
@@ -19,13 +19,23 @@ export async function getCached(config: CloudflareBkndConfig, { env, ctx, ...arg
|
|||||||
...makeCfConfig(config, { env, ctx, ...args }),
|
...makeCfConfig(config, { env, ctx, ...args }),
|
||||||
initialConfig,
|
initialConfig,
|
||||||
onBuilt: async (app) => {
|
onBuilt: async (app) => {
|
||||||
app.module.server.client.get("/__bknd/cache", async (c) => {
|
app.module.server.client.get(constants.cache_endpoint, async (c) => {
|
||||||
await kv.delete(key);
|
await kv.delete(key);
|
||||||
return c.json({ message: "Cache cleared" });
|
return c.json({ message: "Cache cleared" });
|
||||||
});
|
});
|
||||||
await config.onBuilt?.(app);
|
await config.onBuilt?.(app);
|
||||||
},
|
},
|
||||||
beforeBuild: async (app) => {
|
beforeBuild: async (app) => {
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppBeforeResponse,
|
||||||
|
async (event) => {
|
||||||
|
ctx.waitUntil(event.params.app.emgr.executeAsyncs());
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: "sync",
|
||||||
|
id: constants.exec_async_event_id,
|
||||||
|
},
|
||||||
|
);
|
||||||
app.emgr.onEvent(
|
app.emgr.onEvent(
|
||||||
App.Events.AppConfigUpdatedEvent,
|
App.Events.AppConfigUpdatedEvent,
|
||||||
async ({ params: { app } }) => {
|
async ({ params: { app } }) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DurableObject } from "cloudflare:workers";
|
import { DurableObject } from "cloudflare:workers";
|
||||||
import type { App, CreateAppConfig } from "bknd";
|
import { App, type CreateAppConfig } from "bknd";
|
||||||
import { createRuntimeApp, makeConfig } from "bknd/adapter";
|
import { createRuntimeApp, makeConfig } from "bknd/adapter";
|
||||||
import type { CloudflareBkndConfig, Context } from "../index";
|
import { type CloudflareBkndConfig, type Context, constants } from "../index";
|
||||||
|
|
||||||
export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
export async function getDurable(config: CloudflareBkndConfig, ctx: Context) {
|
||||||
const { dobj } = config.bindings?.(ctx.env)!;
|
const { dobj } = config.bindings?.(ctx.env)!;
|
||||||
@@ -67,7 +67,17 @@ export class DurableBkndApp extends DurableObject {
|
|||||||
this.app = await createRuntimeApp({
|
this.app = await createRuntimeApp({
|
||||||
...config,
|
...config,
|
||||||
onBuilt: async (app) => {
|
onBuilt: async (app) => {
|
||||||
app.modules.server.get("/__do", async (c) => {
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppBeforeResponse,
|
||||||
|
async (event) => {
|
||||||
|
this.ctx.waitUntil(event.params.app.emgr.executeAsyncs());
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: "sync",
|
||||||
|
id: constants.exec_async_event_id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
app.modules.server.get(constants.do_endpoint, async (c) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
|
const context: any = c.req.raw.cf ? c.req.raw.cf : c.env.cf;
|
||||||
return c.json({
|
return c.json({
|
||||||
@@ -92,7 +102,6 @@ export class DurableBkndApp extends DurableObject {
|
|||||||
this.keepAlive(options.keepAliveSeconds);
|
this.keepAlive(options.keepAliveSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("id", this.id);
|
|
||||||
const res = await this.app!.fetch(request);
|
const res = await this.app!.fetch(request);
|
||||||
const headers = new Headers(res.headers);
|
const headers = new Headers(res.headers);
|
||||||
headers.set("X-BuildTime", buildtime.toString());
|
headers.set("X-BuildTime", buildtime.toString());
|
||||||
@@ -109,16 +118,13 @@ export class DurableBkndApp extends DurableObject {
|
|||||||
async beforeBuild(app: App) {}
|
async beforeBuild(app: App) {}
|
||||||
|
|
||||||
protected keepAlive(seconds: number) {
|
protected keepAlive(seconds: number) {
|
||||||
console.log("keep alive for", seconds);
|
|
||||||
if (this.interval) {
|
if (this.interval) {
|
||||||
console.log("clearing, there is a new");
|
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
this.interval = setInterval(() => {
|
this.interval = setInterval(() => {
|
||||||
i += 1;
|
i += 1;
|
||||||
//console.log("keep-alive", i);
|
|
||||||
if (i === seconds) {
|
if (i === seconds) {
|
||||||
console.log("cleared");
|
console.log("cleared");
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
|
|||||||
@@ -1,12 +1,25 @@
|
|||||||
import type { App } from "bknd";
|
import { App } from "bknd";
|
||||||
import { createRuntimeApp } from "bknd/adapter";
|
import { createRuntimeApp } from "bknd/adapter";
|
||||||
import { type CloudflareBkndConfig, type Context, makeCfConfig } from "../index";
|
import { type CloudflareBkndConfig, type Context, makeCfConfig, constants } from "../index";
|
||||||
|
|
||||||
export async function makeApp(config: CloudflareBkndConfig, ctx: Context) {
|
export async function makeApp(config: CloudflareBkndConfig, ctx: Context) {
|
||||||
return await createRuntimeApp(
|
return await createRuntimeApp(
|
||||||
{
|
{
|
||||||
...makeCfConfig(config, ctx),
|
...makeCfConfig(config, ctx),
|
||||||
adminOptions: config.html ? { html: config.html } : undefined,
|
adminOptions: config.html ? { html: config.html } : undefined,
|
||||||
|
onBuilt: async (app) => {
|
||||||
|
app.emgr.onEvent(
|
||||||
|
App.Events.AppBeforeResponse,
|
||||||
|
async (event) => {
|
||||||
|
ctx.ctx.waitUntil(event.params.app.emgr.executeAsyncs());
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: "sync",
|
||||||
|
id: constants.exec_async_event_id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await config.onBuilt?.(app);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ctx,
|
ctx,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export class EventManager<
|
|||||||
protected events: EventClass[] = [];
|
protected events: EventClass[] = [];
|
||||||
protected listeners: EventListener[] = [];
|
protected listeners: EventListener[] = [];
|
||||||
enabled: boolean = true;
|
enabled: boolean = true;
|
||||||
|
protected asyncs: (() => Promise<void>)[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
events?: RegisteredEvents,
|
events?: RegisteredEvents,
|
||||||
@@ -29,7 +30,6 @@ export class EventManager<
|
|||||||
listeners?: EventListener[];
|
listeners?: EventListener[];
|
||||||
onError?: (event: Event, e: unknown) => void;
|
onError?: (event: Event, e: unknown) => void;
|
||||||
onInvalidReturn?: (event: Event, e: InvalidEventReturn) => void;
|
onInvalidReturn?: (event: Event, e: InvalidEventReturn) => void;
|
||||||
asyncExecutor?: typeof Promise.all;
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (events) {
|
if (events) {
|
||||||
@@ -176,9 +176,15 @@ export class EventManager<
|
|||||||
this.events.forEach((event) => this.onEvent(event, handler, config));
|
this.events.forEach((event) => this.onEvent(event, handler, config));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected executeAsyncs(promises: (() => Promise<void>)[]) {
|
protected collectAsyncs(promises: (() => Promise<void>)[]) {
|
||||||
const executor = this.options?.asyncExecutor ?? ((e) => Promise.all(e));
|
this.asyncs.push(...promises);
|
||||||
executor(promises.map((p) => p())).then(() => void 0);
|
}
|
||||||
|
|
||||||
|
async executeAsyncs(executor: typeof Promise.all = (e) => Promise.all(e)): Promise<void> {
|
||||||
|
if (this.asyncs.length === 0) return;
|
||||||
|
const asyncs = [...this.asyncs];
|
||||||
|
this.asyncs = [];
|
||||||
|
await executor(asyncs.map((p) => p()));
|
||||||
}
|
}
|
||||||
|
|
||||||
async emit<Actual extends Event<any, any>>(event: Actual): Promise<Actual> {
|
async emit<Actual extends Event<any, any>>(event: Actual): Promise<Actual> {
|
||||||
@@ -209,8 +215,8 @@ export class EventManager<
|
|||||||
return !listener.once;
|
return !listener.once;
|
||||||
});
|
});
|
||||||
|
|
||||||
// execute asyncs
|
// collect asyncs
|
||||||
this.executeAsyncs(asyncs);
|
this.collectAsyncs(asyncs);
|
||||||
|
|
||||||
// execute syncs
|
// execute syncs
|
||||||
let _event: Actual = event;
|
let _event: Actual = event;
|
||||||
|
|||||||
Reference in New Issue
Block a user