Skip to content
20 changes: 19 additions & 1 deletion packages/worker/client/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,15 @@ export function App(handle: Handle<AppProps>) {
handle.props.embeddedSession !== undefined ? 'ready' : 'idle'
let sessionRefreshInFlight = false
let sessionRefreshQueued = false
let lastSessionRefreshAt = 0
let currentPathname = readRouterPathname(handle)

// Navigation-triggered refreshes are throttled: auth rarely changes
// mid-session and every SPA navigation was previously a /session round
// trip (2 D1 queries). Explicit refreshes (login/logout/profile updates
// via setSessionRefreshHandler) always bypass the throttle.
const sessionRefreshThrottleMs = 30_000

function queueSessionRefresh() {
sessionRefreshQueued = true
if (sessionRefreshInFlight) return
Expand All @@ -44,6 +51,7 @@ export function App(handle: Handle<AppProps>) {
const nextSession = await fetchSessionInfo(signal)
sessionRefreshInFlight = false
if (signal.aborted) return
lastSessionRefreshAt = Date.now()
session = nextSession
sessionStatus = 'ready'
handle.update()
Expand All @@ -56,6 +64,16 @@ export function App(handle: Handle<AppProps>) {
}
}

function queueThrottledSessionRefresh() {
if (
sessionStatus === 'ready' &&
Date.now() - lastSessionRefreshAt < sessionRefreshThrottleMs
) {
return
}
queueSessionRefresh()
}

// Always revalidate after hydration: the embedded session renders the
// first paint without a flash ('ready' status keeps the refresh silent),
// but auth may have changed since the document was rendered.
Expand All @@ -66,7 +84,7 @@ export function App(handle: Handle<AppProps>) {
})
listenToRouterNavigation(handle, () => {
currentPathname = readRouterPathname(handle)
queueSessionRefresh()
queueThrottledSessionRefresh()
handle.update()
})
}
Expand Down
37 changes: 19 additions & 18 deletions packages/worker/src/app/admin-users-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,26 +37,27 @@ export async function loadAdminUsersData(
)
const offset = (page - 1) * pageSize

const totalResult = await env.APP_DB.prepare(
`SELECT COUNT(*) AS total FROM users`,
).first<{ total: number }>()
const [totalResult, userRows] = await Promise.all([
env.APP_DB.prepare(`SELECT COUNT(*) AS total FROM users`).first<{
total: number
}>(),
env.APP_DB.prepare(
`SELECT id, username, email, created_at, updated_at
FROM users
ORDER BY id ASC
LIMIT ? OFFSET ?`,
)
.bind(pageSize, offset)
.all<{
id: number
username: string
email: string
created_at: string
updated_at: string
}>(),
])
const total = totalResult?.total ?? 0

const userRows = await env.APP_DB.prepare(
`SELECT id, username, email, created_at, updated_at
FROM users
ORDER BY id ASC
LIMIT ? OFFSET ?`,
)
.bind(pageSize, offset)
.all<{
id: number
username: string
email: string
created_at: string
updated_at: string
}>()

const userIds = (userRows.results ?? []).map((row) => row.id)
const rolesByUserId = await loadRolesByUserIds(env.APP_DB, userIds)

Expand Down
28 changes: 27 additions & 1 deletion packages/worker/src/app/community-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,33 @@ export async function loadCommunityIndexData(
}
}

export async function loadCommunityDetailData(
// One SSR request loads detail data twice: once in the HTML handler for the
// loaderData embed and once in the frame renderer during streaming. Memoize
// per Request so the second call reuses the first load.
const requestDetailDataStore = new WeakMap<
Request,
Map<string, Promise<CommunityDetailLoaderData | null>>
>()

export function loadCommunityDetailData(
env: Env,
request: Request,
listingId: string,
): Promise<CommunityDetailLoaderData | null> {
let byListingId = requestDetailDataStore.get(request)
if (!byListingId) {
byListingId = new Map()
requestDetailDataStore.set(request, byListingId)
}
let pending = byListingId.get(listingId)
if (!pending) {
pending = loadCommunityDetailDataUncached(env, request, listingId)
byListingId.set(listingId, pending)
}
return pending
}

async function loadCommunityDetailDataUncached(
env: Env,
request: Request,
listingId: string,
Expand Down
12 changes: 12 additions & 0 deletions packages/worker/src/app/env.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { parseSafe } from 'remix/data-schema'
import { EnvSchema, type AppEnv } from '#worker/env-schema.ts'

// Env binding objects are stable per isolate; parse them once per identity
// instead of running schema validation on every call.
const parsedEnvCache = new WeakMap<Env, AppEnv>()

export function getEnv(env: Env): AppEnv {
const cached = parsedEnvCache.get(env)
if (cached) return cached
const parsed = parseEnv(env)
parsedEnvCache.set(env, parsed)
return parsed
}

function parseEnv(env: Env): AppEnv {
const result = parseSafe(EnvSchema, env)

if (!result.success) {
Expand Down
23 changes: 23 additions & 0 deletions packages/worker/src/app/handler.node.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from 'vitest'
import { getEnv } from './env.ts'
import { handleRequest } from './handler.ts'

function createEnv(overrides: Record<string, unknown> = {}) {
Expand Down Expand Up @@ -31,3 +32,25 @@ test('account secrets api ignores legacy remote connector env secrets', async ()
error: 'Unauthorized.',
})
})

test('getEnv memoizes the parsed env per env object identity', () => {
const env = createEnv()
expect(getEnv(env)).toBe(getEnv(env))
expect(getEnv(createEnv())).not.toBe(getEnv(env))
})

test('handleRequest serves multiple requests from the same env object', async () => {
const env = createEnv()
const first = await handleRequest(
new Request('https://example.com/health'),
env,
)
const second = await handleRequest(
new Request('https://example.com/health'),
env,
)

expect(first.status).toBe(200)
expect(second.status).toBe(200)
await expect(second.json()).resolves.toMatchObject({ ok: true })
})
25 changes: 23 additions & 2 deletions packages/worker/src/app/handler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
import { setAuthSessionSecret } from '#app/auth-session.ts'
import { getEnv } from '#app/env.ts'
import { createAppRouter } from '#app/router.ts'
import { type AppEnv } from '#worker/env-schema.ts'

type AppRouterBundle = {
appEnv: AppEnv
router: ReturnType<typeof createAppRouter>
}

// Env binding objects are stable per isolate, so the parsed env and the
// router (which closes over env, not the request) can be built once instead
// of on every request. Keyed by identity so a new env object (new isolate or
// updated bindings) gets a fresh router.
const appRouterCache = new WeakMap<Env, AppRouterBundle>()

function getAppRouterBundle(env: Env): AppRouterBundle {
let bundle = appRouterCache.get(env)
if (!bundle) {
const appEnv = getEnv(env)
bundle = { appEnv, router: createAppRouter(appEnv) }
appRouterCache.set(env, bundle)
}
return bundle
}

export async function handleRequest(request: Request, env: Env) {
try {
const appEnv = getEnv(env)
const { appEnv, router } = getAppRouterBundle(env)
setAuthSessionSecret(appEnv.COOKIE_SECRET)
const router = createAppRouter(appEnv)
return await router.fetch(request)
} catch (error) {
console.error('Remix server handler failed:', error)
Expand Down
16 changes: 9 additions & 7 deletions packages/worker/src/app/handlers/package-app.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ const mockModule = vi.hoisted(() => ({
loadPackageSourceBySourceId: vi.fn(async () => {
throw new Error('bundle failed')
}),
loadPackageManifestBySourceId: vi.fn(async () => {
throw new Error('manifest load failed')
}),
createPackageAppCallerContext: vi.fn(),
buildPackageAppWorker: vi.fn(),
packageRealtimeConnect: vi.fn(
Expand Down Expand Up @@ -79,6 +82,8 @@ vi.mock('#worker/package-registry/repo.ts', () => ({
vi.mock('#worker/package-registry/source.ts', () => ({
loadPackageSourceBySourceId: (...args: Array<unknown>) =>
mockModule.loadPackageSourceBySourceId(...args),
loadPackageManifestBySourceId: (...args: Array<unknown>) =>
mockModule.loadPackageManifestBySourceId(...args),
}))

vi.mock('#worker/package-runtime/package-app.ts', () => ({
Expand Down Expand Up @@ -182,18 +187,15 @@ test('handlePackageAppRequest reports host setup failures with helpful responses
test('handlePackageAppRequest does not report package entrypoint failures to Kody Sentry', async () => {
resetMocks()

mockModule.loadPackageSourceBySourceId.mockResolvedValueOnce({
mockModule.loadPackageManifestBySourceId.mockResolvedValueOnce({
source: {
published_commit: 'commit-1',
manifest_path: 'package.json',
source_root: '/',
},
files: {
'package.json': JSON.stringify({
name: '@kody/example',
kody: { id: 'example', app: { entry: 'app.js' } },
}),
'app.js': 'export default {}',
manifest: {
name: '@kody/example',
kody: { id: 'example', app: { entry: 'app.js' } },
},
})
mockModule.buildPackageAppWorker.mockResolvedValueOnce({
Expand Down
55 changes: 35 additions & 20 deletions packages/worker/src/app/handlers/package-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { redirectToLoginWhenUnauthenticated } from '#app/auth-redirect.ts'
import { getAppBaseUrl } from '#app/app-base-url.ts'
import { getUsernameValidationError } from '#app/username.ts'
import { getSavedPackageByKodyId } from '#worker/package-registry/repo.ts'
import { loadPackageSourceBySourceId } from '#worker/package-registry/source.ts'
import {
loadPackageManifestBySourceId,
loadPackageSourceBySourceId,
} from '#worker/package-registry/source.ts'
import {
buildPackageAppWorker,
createPackageAppCallerContext,
Expand Down Expand Up @@ -295,22 +298,24 @@ export async function handlePackageAppRequest(request: Request, env: Env) {
let forwardedRequest: Request
let entrypoint: { fetch(request: Request): Promise<Response> }
try {
const packageSource = await loadPackageSourceBySourceId({
env,
baseUrl,
userId: user.mcpUser.userId,
sourceId: savedPackage.sourceId,
})
const callerContext = await createPackageAppCallerContext({
baseUrl,
user: {
const [packageManifest, callerContext] = await Promise.all([
loadPackageManifestBySourceId({
env,
baseUrl,
userId: user.mcpUser.userId,
email: user.email,
displayName: user.displayName,
username: user.username,
},
packageId: savedPackage.id,
})
sourceId: savedPackage.sourceId,
}),
createPackageAppCallerContext({
baseUrl,
user: {
userId: user.mcpUser.userId,
email: user.email,
displayName: user.displayName,
username: user.username,
},
packageId: savedPackage.id,
}),
])
const appWorker = await buildPackageAppWorker({
env,
baseUrl,
Expand All @@ -320,11 +325,21 @@ export async function handlePackageAppRequest(request: Request, env: Env) {
kodyId: savedPackage.kodyId,
name: savedPackage.name,
sourceId: savedPackage.sourceId,
publishedCommit: packageSource.source.published_commit,
manifestPath: packageSource.source.manifest_path,
sourceRoot: packageSource.source.source_root,
publishedCommit: packageManifest.source.published_commit,
manifestPath: packageManifest.source.manifest_path,
sourceRoot: packageManifest.source.source_root,
},
source: packageManifest.source,
manifest: packageManifest.manifest,
loadSourceFiles: async () => {
const packageSource = await loadPackageSourceBySourceId({
env,
baseUrl,
userId: user.mcpUser.userId,
sourceId: savedPackage.sourceId,
})
return packageSource.files
},
sourceFiles: packageSource.files,
runtime: {
callerContext,
},
Expand Down
Loading
Loading