mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 14:41:28 -07:00
use path prefixes to make it easy
This commit is contained in:
+20
-13
@@ -7,9 +7,11 @@
|
||||
* the request's subdomain (`accounts.<domain>` -> the `accounts` app), so the sub-app
|
||||
* paths (and therefore the client contract) are untouched.
|
||||
*
|
||||
* Local dev has no subdomain, so pick a service explicitly with the
|
||||
* `X-Recflare-Service` header or an `?__svc=` query param, e.g.
|
||||
* curl -H 'X-Recflare-Service: accounts' http://localhost:8787/health
|
||||
* Local dev has no subdomain, so the first path segment selects the service and is
|
||||
* stripped before the request is forwarded, e.g.
|
||||
* http://localhost:8787/accounts/ -> accounts app sees /
|
||||
* http://localhost:8787/match/player/login -> match app sees /player/login
|
||||
* http://localhost:8787/api/api/config/v2 -> api app sees /api/config/v2
|
||||
*
|
||||
* NOT mounted here: `www`, `img`, `econ`. Each binds a static `assets` directory and
|
||||
* Cloudflare allows only one static-assets binding per Worker. Resolve that (serve
|
||||
@@ -62,34 +64,39 @@ const services = {
|
||||
|
||||
type ServiceName = keyof typeof services
|
||||
|
||||
function resolveService(request: Request): ServiceName | undefined {
|
||||
function resolve(request: Request): { name: ServiceName; request: Request } | undefined {
|
||||
const url = new URL(request.url)
|
||||
|
||||
// Local-dev / explicit override (localhost has no service subdomain).
|
||||
const override = request.headers.get('x-recflare-service') ?? url.searchParams.get('__svc')
|
||||
if (override !== null && override in services) return override as ServiceName
|
||||
|
||||
// Production: dispatch on the leftmost DNS label — accounts.<domain> -> accounts.
|
||||
// The path is forwarded unchanged so the client contract is identical.
|
||||
const sub = url.hostname.split('.')[0]
|
||||
if (sub in services) return sub as ServiceName
|
||||
if (sub in services) return { name: sub as ServiceName, request }
|
||||
|
||||
// Local dev (no service subdomain): the first path segment selects the service and
|
||||
// is stripped before forwarding — /match/player/login -> match app sees /player/login.
|
||||
const [, first, ...rest] = url.pathname.split('/')
|
||||
if (first !== undefined && first in services) {
|
||||
url.pathname = `/${rest.join('/')}`
|
||||
return { name: first as ServiceName, request: new Request(url, request) }
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export default {
|
||||
fetch(request: Request, env: Env, ctx: ExecutionContext): Response | Promise<Response> {
|
||||
const name = resolveService(request)
|
||||
if (name === undefined) {
|
||||
const resolved = resolve(request)
|
||||
if (resolved === undefined) {
|
||||
return Response.json(
|
||||
{
|
||||
error: 'unknown_service',
|
||||
hint: 'Route by subdomain (<service>.<domain>). In local dev set the X-Recflare-Service header or ?__svc= query.',
|
||||
hint: 'Route by subdomain (<service>.<domain>), or in local dev prefix the path with the service name (/<service>/...).',
|
||||
services: Object.keys(services),
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
return services[name].fetch(request, env, ctx)
|
||||
return services[resolved.name].fetch(resolved.request, env, ctx)
|
||||
},
|
||||
|
||||
// Only `match` runs a cron in the split deployment; this worker owns its presence sweep.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { exports } from 'cloudflare:workers'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
import type { Env } from '../../context'
|
||||
|
||||
declare module 'cloudflare:test' {
|
||||
interface ProvidedEnv extends Env {}
|
||||
}
|
||||
|
||||
const ORIGIN = 'https://example.com'
|
||||
|
||||
// The facade's job is routing, not business logic, so one request that reaches a
|
||||
// mounted app through the path prefix is enough to prove the wiring. `api` serves a
|
||||
// static game-config with no auth/DB, so it's a clean target. The api worker namespaces
|
||||
// its own routes under `/api`, hence the `/api` prefix (service) + `/api/...` (real path).
|
||||
describe('mono routing', () => {
|
||||
test('path prefix routes to the api worker (gameconfigs)', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/api/api/gameconfigs/v1/all`)
|
||||
expect(res.status).toBe(200)
|
||||
// Reached the api app's real handler, not the facade's 404.
|
||||
expect(res.headers.get('content-type')).toContain('application/json')
|
||||
})
|
||||
|
||||
test('unknown service prefix returns the facade 404', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/nope/whatever`)
|
||||
expect(res.status).toBe(404)
|
||||
expect(await res.json()).toMatchObject({ error: 'unknown_service' })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user