diff --git a/examples/basic-worker/src/index.ts b/examples/basic-worker/src/index.ts index 37cb720..410c731 100644 --- a/examples/basic-worker/src/index.ts +++ b/examples/basic-worker/src/index.ts @@ -4,17 +4,17 @@ import { cfProvider } from "@mvrx/mail/providers"; import { classify } from "@mvrx/mail/ai-tools"; import { compose } from "@mvrx/mail/compose"; import { processors } from "@mvrx/mail/attachments"; -import { publishEvent, hubRouter } from "@mvrx/mail/hub"; +import { publishEvent, relayRouter } from "@mvrx/mail/relay"; -// Register the UserHub Durable Object (backs real-time SSE events). -export { UserHub } from "@mvrx/mail/hub"; +// Register the UserRelay Durable Object (backs real-time SSE events). +export { UserRelay } from "@mvrx/mail/relay"; interface Env { DB: D1Database; BLOBS: R2Bucket; AI: Ai; EMAIL: SendEmail; - HUB: DurableObjectNamespace; + RELAY: DurableObjectNamespace; AGENT_MODEL_CLASSIFY: string; AGENT_MODEL_CHAT: string; } @@ -43,7 +43,7 @@ export default { const results = await evaluateRules(email, rules, cfTransport(env.EMAIL)); for (const r of results) { if (!r.matched) continue; - await publishEvent(env.HUB, userId, { + await publishEvent(env.RELAY, userId, { type: "rule_fired", payload: { ruleId: r.ruleId, @@ -55,7 +55,7 @@ export default { } // Push a real-time "new message" event to any connected SSE clients. - await publishEvent(env.HUB, userId, { + await publishEvent(env.RELAY, userId, { type: "new_message", payload: { messageId: email.messageId, @@ -90,13 +90,13 @@ export default { ); }, - // Mount the real-time SSE endpoint: clients connect with `new EventSource("/hub")`. + // Mount the real-time SSE endpoint: clients connect with `new EventSource("/relay")`. async fetch(req: Request, env: Env): Promise { const url = new URL(req.url); - if (url.pathname === "/hub") { + if (url.pathname === "/relay") { // Derive the userId from your auth in production; single-tenant demo below. const userId = url.searchParams.get("user") ?? "demo"; - return hubRouter(req, env.HUB, userId); + return relayRouter(req, env.RELAY, userId); } return new Response("AECS mail Worker — receive, store, rules, events, classify, reply"); }, diff --git a/examples/basic-worker/wrangler.jsonc b/examples/basic-worker/wrangler.jsonc index a1a3f26..c66e8af 100644 --- a/examples/basic-worker/wrangler.jsonc +++ b/examples/basic-worker/wrangler.jsonc @@ -25,15 +25,15 @@ "durable_objects": { "bindings": [ { - "name": "HUB", - "class_name": "UserHub" + "name": "RELAY", + "class_name": "UserRelay" } ] }, "migrations": [ { "tag": "v1", - "new_sqlite_classes": ["UserHub"] + "new_sqlite_classes": ["UserRelay"] } ], diff --git a/packages/mail/package.json b/packages/mail/package.json index a66f5e9..eb919c0 100644 --- a/packages/mail/package.json +++ b/packages/mail/package.json @@ -62,9 +62,9 @@ "import": "./dist/attachments/index.js", "types": "./dist/attachments/index.d.ts" }, - "./hub": { - "import": "./dist/hub/index.js", - "types": "./dist/hub/index.d.ts" + "./relay": { + "import": "./dist/relay/index.js", + "types": "./dist/relay/index.d.ts" } }, "scripts": { diff --git a/packages/mail/src/adapters.ts b/packages/mail/src/adapters.ts index aaaf17b..52dd5d6 100644 --- a/packages/mail/src/adapters.ts +++ b/packages/mail/src/adapters.ts @@ -86,7 +86,7 @@ export interface MailEvent { /** * Abstracts real-time fan-out to connected clients. - * CF implementation: UserHub Durable Object via `hubBus(env.HUB)` in `@mvrx/mail/hub`. + * CF implementation: UserRelay Durable Object via `relayBus(env.RELAY)` in `@mvrx/mail/relay`. * For non-CF deployments: implement with WebSockets, SSE, or webhooks. */ export interface NotificationBus { diff --git a/packages/mail/src/hub/index.ts b/packages/mail/src/relay/index.ts similarity index 82% rename from packages/mail/src/hub/index.ts rename to packages/mail/src/relay/index.ts index d9674d7..5f0b8fd 100644 --- a/packages/mail/src/hub/index.ts +++ b/packages/mail/src/relay/index.ts @@ -5,9 +5,9 @@ import type { MailEvent as LooseMailEvent, NotificationBus } from "../adapters.j /** * Real-time events for @mvrx/mail (AECS-SDK-1 §16). * - * `UserHub` is a Durable Object — one instance per user, keyed by an opaque + * `UserRelay` is a Durable Object — one instance per user, keyed by an opaque * `userId` — that holds open Server-Sent Events (SSE) connections and fans out - * `MailEvent`s to them. `publishEvent`/`hubRouter`/`hubBus` are the SDK helpers + * `MailEvent`s to them. `publishEvent`/`relayRouter`/`relayBus` are the SDK helpers * that route through the DO. * * Delivery is fire-and-forget, at-most-once, with NO replay (§16.5): if no @@ -47,7 +47,7 @@ export type MailEvent = }; // The typed union above must stay assignable to the looser adapters.ts MailEvent -// ({ type: MailEventType; payload: Record }) so UserHub can +// ({ type: MailEventType; payload: Record }) so UserRelay can // satisfy NotificationBus. This assertion fails the build if they ever diverge. const _assertAssignable: LooseMailEvent = null as unknown as MailEvent; void _assertAssignable; @@ -64,14 +64,14 @@ export function toSseFrame(event: MailEvent): Uint8Array { const KEEPALIVE_MS = 25_000; const KEEPALIVE_FRAME = encoder.encode(": keep-alive\n\n"); -// ── UserHub Durable Object ─────────────────────────────────────────────────── +// ── UserRelay Durable Object ─────────────────────────────────────────────────── /** * One instance per user. Holds the set of open SSE stream controllers and fans * `MailEvent`s out to them. Register the class in wrangler with a DO binding + - * migration (see the example worker), then `export { UserHub } from "@mvrx/mail/hub"`. + * migration (see the example worker), then `export { UserRelay } from "@mvrx/mail/relay"`. */ -export class UserHub extends DurableObject { +export class UserRelay extends DurableObject { private controllers = new Set>(); private keepAlive: ReturnType | null = null; @@ -145,8 +145,8 @@ export class UserHub extends DurableObject { // ── SDK helpers ────────────────────────────────────────────────────────────── -function stubFor(hub: DurableObjectNamespace, userId: string): DurableObjectStub { - return hub.get(hub.idFromName(userId)); +function stubFor(relay: DurableObjectNamespace, userId: string): DurableObjectStub { + return relay.get(relay.idFromName(userId)); } /** @@ -155,11 +155,11 @@ function stubFor(hub: DurableObjectNamespace, userId: string): DurableObjectStub * Worker handler. */ export async function publishEvent( - hub: DurableObjectNamespace, + relay: DurableObjectNamespace, userId: string, event: MailEvent ): Promise { - await stubFor(hub, userId).fetch("https://user-hub/publish", { + await stubFor(relay, userId).fetch("https://user-relay/publish", { method: "POST", body: JSON.stringify(event), }); @@ -168,22 +168,22 @@ export async function publishEvent( /** * Mount as an SSE endpoint. Returns a `text/event-stream` Response that stays * open and streams this user's `MailEvent`s. Wire it into your fetch handler: - * `if (url.pathname === "/hub") return hubRouter(req, env.HUB, getUserId(req))`. + * `if (url.pathname === "/relay") return relayRouter(req, env.RELAY, getUserId(req))`. */ -export async function hubRouter( +export async function relayRouter( _req: Request, - hub: DurableObjectNamespace, + relay: DurableObjectNamespace, userId: string ): Promise { - return stubFor(hub, userId).fetch("https://user-hub/connect"); + return stubFor(relay, userId).fetch("https://user-relay/connect"); } /** - * Adapts a UserHub DO namespace to the `NotificationBus` interface (adapters.ts), + * Adapts a UserRelay DO namespace to the `NotificationBus` interface (adapters.ts), * so it can be passed anywhere a generic bus is expected. */ -export function hubBus(hub: DurableObjectNamespace): NotificationBus { +export function relayBus(relay: DurableObjectNamespace): NotificationBus { return { - publish: (userId, event) => publishEvent(hub, userId, event as MailEvent), + publish: (userId, event) => publishEvent(relay, userId, event as MailEvent), }; } diff --git a/packages/mail/test/env.d.ts b/packages/mail/test/env.d.ts index 37311c9..013646f 100644 --- a/packages/mail/test/env.d.ts +++ b/packages/mail/test/env.d.ts @@ -4,6 +4,6 @@ declare module "cloudflare:test" { DB: D1Database; BLOBS: R2Bucket; CACHE: KVNamespace; - HUB: DurableObjectNamespace; + RELAY: DurableObjectNamespace; } } diff --git a/packages/mail/test/hub.test.ts b/packages/mail/test/relay.test.ts similarity index 63% rename from packages/mail/test/hub.test.ts rename to packages/mail/test/relay.test.ts index 71498c7..3cac150 100644 --- a/packages/mail/test/hub.test.ts +++ b/packages/mail/test/relay.test.ts @@ -1,6 +1,6 @@ import { env, runInDurableObject } from "cloudflare:test"; import { describe, it, expect } from "vitest"; -import { UserHub, publishEvent, hubRouter, hubBus, toSseFrame, type MailEvent } from "../src/hub/index.js"; +import { UserRelay, publishEvent, relayRouter, relayBus, toSseFrame, type MailEvent } from "../src/relay/index.js"; const newMessage: MailEvent = { type: "new_message", @@ -25,22 +25,22 @@ describe("toSseFrame", () => { }); }); -describe("UserHub", () => { +describe("UserRelay", () => { it("drops events when no client is connected (fire-and-forget)", async () => { - await publishEvent(env.HUB, "nobody@example.com", newMessage); // must not throw - const stub = env.HUB.get(env.HUB.idFromName("nobody@example.com")); - const count = await runInDurableObject(stub, (instance: UserHub) => instance.connectionCount()); + await publishEvent(env.RELAY, "nobody@example.com", newMessage); // must not throw + const stub = env.RELAY.get(env.RELAY.idFromName("nobody@example.com")); + const count = await runInDurableObject(stub, (instance: UserRelay) => instance.connectionCount()); expect(count).toBe(0); }); it("delivers a published event to a connected SSE client", async () => { - const res = await hubRouter(new Request("https://worker/hub"), env.HUB, "u1@example.com"); + const res = await relayRouter(new Request("https://worker/relay"), env.RELAY, "u1@example.com"); expect(res.headers.get("content-type")).toContain("text/event-stream"); expect(res.body).not.toBeNull(); const reader = res.body!.getReader(); - await publishEvent(env.HUB, "u1@example.com", newMessage); + await publishEvent(env.RELAY, "u1@example.com", newMessage); const { value } = await reader.read(); const frame = decode(value!); @@ -51,16 +51,16 @@ describe("UserHub", () => { }); it("routes different userIds to different DO instances", async () => { - const resA = await hubRouter(new Request("https://worker/hub"), env.HUB, "a@example.com"); + const resA = await relayRouter(new Request("https://worker/relay"), env.RELAY, "a@example.com"); const readerA = resA.body!.getReader(); // Publish only to user b — user a's stream should NOT receive it. - await publishEvent(env.HUB, "b@example.com", newMessage); + await publishEvent(env.RELAY, "b@example.com", newMessage); - const stubA = env.HUB.get(env.HUB.idFromName("a@example.com")); - const stubB = env.HUB.get(env.HUB.idFromName("b@example.com")); - const countA = await runInDurableObject(stubA, (i: UserHub) => i.connectionCount()); - const countB = await runInDurableObject(stubB, (i: UserHub) => i.connectionCount()); + const stubA = env.RELAY.get(env.RELAY.idFromName("a@example.com")); + const stubB = env.RELAY.get(env.RELAY.idFromName("b@example.com")); + const countA = await runInDurableObject(stubA, (i: UserRelay) => i.connectionCount()); + const countB = await runInDurableObject(stubB, (i: UserRelay) => i.connectionCount()); expect(countA).toBe(1); // a is connected expect(countB).toBe(0); // b never connected @@ -68,9 +68,9 @@ describe("UserHub", () => { }); }); -describe("hubBus", () => { +describe("relayBus", () => { it("adapts the namespace to a NotificationBus", async () => { - const bus = hubBus(env.HUB); + const bus = relayBus(env.RELAY); await bus.publish("c@example.com", newMessage); // must not throw expect(typeof bus.publish).toBe("function"); }); diff --git a/packages/mail/test/worker.ts b/packages/mail/test/worker.ts index 4a34229..6308f62 100644 --- a/packages/mail/test/worker.ts +++ b/packages/mail/test/worker.ts @@ -1,6 +1,6 @@ -// Test-only Worker entry: the vitest-pool-workers `main` so the UserHub Durable -// Object class is registered and reachable through the HUB binding in tests. -export { UserHub } from "../src/hub/index.js"; +// Test-only Worker entry: the vitest-pool-workers `main` so the UserRelay Durable +// Object class is registered and reachable through the RELAY binding in tests. +export { UserRelay } from "../src/relay/index.js"; export default { async fetch(): Promise { diff --git a/packages/mail/vitest.config.ts b/packages/mail/vitest.config.ts index 22b5b38..7fe1b3b 100644 --- a/packages/mail/vitest.config.ts +++ b/packages/mail/vitest.config.ts @@ -6,7 +6,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ plugins: [ cloudflareTest({ - // Worker entry that registers the UserHub Durable Object so the HUB + // Worker entry that registers the UserRelay Durable Object so the RELAY // binding resolves to it in tests. main: "./test/worker.ts", miniflare: { @@ -15,7 +15,7 @@ export default defineConfig({ d1Databases: ["DB"], r2Buckets: ["BLOBS"], kvNamespaces: ["CACHE"], - durableObjects: { HUB: "UserHub" }, + durableObjects: { RELAY: "UserRelay" }, }, }), ],