Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@corbits/last-30-days-research-workflow": "workspace:*",
"@corbits/mailbox": "github:corbitsdev/corbits-mailbox#e85b81e4a2e0cc318dd33a4f0f92337b6fac7f38",
"@corbits/mcp-tools": "workspace:*",
"@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3",
"@corbits/memory": "github:corbitsdev/corbits-memory#e74da20f148a302dff5400915fe504ee2395e913",
"@corbits/morning-brief-workflow": "workspace:*",
"@corbits/notify": "workspace:*",
"@corbits/pain-point-collateral-workflow": "workspace:*",
Expand Down
69 changes: 69 additions & 0 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,15 @@ import {
import { reportError } from "@corbits/error-sink";
import { decodedOrNull } from "@corbits/url-path";
import {
buildMailFrame,
createInMemoryMailboxEventBus,
createMailboxDb,
createMailboxPersist,
generateMailboxMessageId,
mountMailbox,
} from "@corbits/mailbox";
import { createMemory, loadMemoryConfig } from "@corbits/memory";
import { applyCronMigrations, createCronTicker, mountCron } from "@corbits/cron";
import {
createHubMailboxAuthorizeSender,
createHubPersistMailWithSessionEnsure,
Expand Down Expand Up @@ -546,6 +549,9 @@ export async function createHub(config: HubConfig) {
// Package-owned artifacts tables (CL-8188): idempotent, advisory-locked,
// safe on every boot of every replica — see @corbits/artifacts' README.
await runArtifactMigrations(db);
// `@corbits/cron`'s own schema (CL-8183): idempotent, its own migration
// ledger — see the package's README.
await applyCronMigrations(config.databaseUrl);
const artifactContentStore = InlineContentStore;
const baseEventCollectors = createEventCollectorRegistry({
db,
Expand Down Expand Up @@ -881,6 +887,68 @@ export async function createHub(config: HubConfig) {
app.route(`${TENANT_PREFIX}/mailbox`, mailboxApp);
}

let cronTicker: { start(): void; stop(): void } | undefined;
{
// Cron-to-mail bridge (CL-8183): `@corbits/cron` registers its own
// absolute `/api/tenants/:tenantId/cron` routes, so it gets its own
// `Hono<TenantEnv>` routed at "/" — matching `mailboxApp` above —
// rather than `TENANT_PREFIX`, which would double the tenant-route
// prefix. `requireTenantMember` reads the tenant middleware's context
// exactly like the mailbox mount's `resolvePrincipal` above.
const cronApp = new Hono<TenantEnv>();
mountCron(cronApp, {
db,
requireTenantMember: (ctx, tenantId) => {
const c = ctx as { get(key: "tenant"): { id: string } };
return c.get("tenant").id === tenantId;
},
});
app.route("/", cronApp);

// The ticker delivers each due schedule as mail through the same
// `lookups.persistMail` every other outbound frame in this hub already
// dispatches through (see the mailbox mount's own `deliver` above).
// `@corbits/cron` hands back only `{to, subject, body, from}`, not a
// raw frame, so this hub builds the RFC 5322 message itself with
// `@corbits/mailbox`'s own `buildMailFrame` — the same builder that
// package uses for every frame it authors. `senderAddressFor` addresses
// the sender as `cron@<tenant domain>`, the same domain-scoped shape
// every other mailbox writer in this file uses (see the mailbox
// mount's `senderAddressFor` above), by looking the domain up per
// tenant the way that one does.
cronTicker = createCronTicker({
db,
intervalMs: 60_000,
senderAddressFor: (tenantId) => `cron@${tenantId}`,
deliver: async (message) => {
// `message.from` is `cron@<tenantId>` from `senderAddressFor`
// above; resolve the real tenant domain to address it from.
const tenantId = message.from.slice("cron@".length);
const [tenantRow] = await db
.select({ domain: tenantTable.domain })
.from(tenantTable)
.where(eq(tenantTable.id, tenantId))
.limit(1);
if (tenantRow === undefined) {
throw new Error(`no tenant "${tenantId}" to address cron mail from`);
}
const from = `cron@${tenantRow.domain}`;
await lookups.persistMail({
senderAddress: from,
recipients: message.to,
raw: buildMailFrame({
from,
to: message.to.join(", "),
subject: message.subject,
body: message.body,
messageId: generateMailboxMessageId(from),
}),
});
},
});
cronTicker.start();
}

{
// Firm memory (CL-8186): `@corbits/memory` registers its own
// absolute `/api/tenants/:tenantId/memory/*` routes on whatever
Expand Down Expand Up @@ -1149,6 +1217,7 @@ export async function createHub(config: HubConfig) {
clearTimeout(sidecarAllocationReconciliationTimer);
}
await closeMailbox();
cronTicker?.stop();
// The pool end waits on in-flight queries; a query whose socket
// died with the process must never stall shutdown, so bound it.
// (CL-7584: a fire-and-forget reconcile's request can be cut
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion packages/cron/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ export {
type CronField,
type ZonedParts,
} from "./cron";
export { cronScheduleTable } from "./schema";
export {
cronScheduleTable,
cronMigrations,
applyCronMigrations,
type CronMigration,
type ApplyCronMigrationsReport,
} from "./schema";
export {
createCronTicker,
cronSenderAddress,
Expand Down
Loading