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
8 changes: 8 additions & 0 deletions .agents/upstream-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ Two standing sections outlive any single batch and must be read on every review:

## Review batches

## 2026-09-06 — stream and cache static responses without stale HTML (partial source)

Adopts upstream #9669, `27e6cc27fe0f3cff53a44905e40615b7db99c80c`, plus the HTML validator correction from #9799, `ce4712d5b04fb998f79fe132245289191147e5d5`, under the maintainer's standing approval. Complete source patches, PR descriptions, and relevant reviews were read. #9799 is one source commit shared with the separately adopted live-stream fix; its remaining mobile, highlighting, animation, marketing, and CI concerns are not included. The full review cursor remains unchanged.

Static responses stream bounded chunks from one request-scoped handle, using that same handle for metadata. Immutable caching requires both a hashed asset name and membership in the Vite build manifest. Mutable assets can revalidate; HTML always returns the current shell without size/mtime validators, including SPA fallback and conditional HEAD requests. This preserves Pylon authentication, routing, compression, desktop identity, and remote origins. Hosted clients use their hosting layer; the bundled and standalone Pylon servers share this route. No mobile native dependency changes.

Verification: the same-size/same-timestamp HTML regression failed with a stale 304 before the correction. All 174 server routing tests pass after integration, including atomic file replacement, GET/HEAD/304/cancellation handle cleanup, manifest lookalikes, authentication, and live streams. Server and web typechecks, targeted lint, formatting, and diff checks pass. Implementation branch: `upstream/2026-09-06-static-response-caching` based on `origin/pylon` at `1e7ffff246bf6ad4c31236e6b242a91d5c2275b0`.

## 2026-09-06 — project live updates before applying buffer limits (partial source)

Adopted the live-stream portion of upstream #9799, `ce4712d5b04fb998f79fe132245289191147e5d5`, under the maintainer's standing approval for compatible fixes. The complete upstream patch, PR description, and review context were read. This port changes only the live budget/coalescer, shell stream metadata, and focused WebSocket regression tests. The upstream HTML, mobile outbox, highlighting, animation, marketing, and CI changes are separate concerns and are not claimed as adopted here. The full range cursor remains unchanged.
Expand Down
148 changes: 126 additions & 22 deletions apps/server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import { cast } from "effect/Function";
import {
Expand Down Expand Up @@ -431,10 +432,66 @@ export const attachmentUploadRouteLayer = HttpRouter.add(
}),
);

export const staticAndDevRouteLayer = HttpRouter.add(
"GET",
"*",
Effect.gen(function* () {
const decodeBuildManifest = Schema.decodeUnknownEffect(
Schema.fromJsonString(
Schema.Record(
Schema.String,
Schema.Struct({
file: Schema.String,
css: Schema.optional(Schema.Array(Schema.String)),
assets: Schema.optional(Schema.Array(Schema.String)),
}),
),
),
);

const loadImmutableBuildAssets = Effect.gen(function* () {
const config = yield* ServerConfig.ServerConfig;
const staticDir =
config.staticDir ?? (config.devUrl ? yield* ServerConfig.resolveStaticDir() : undefined);
if (!staticDir) return new Set<string>();
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
return yield* fileSystem.readFileString(path.join(staticDir, ".vite", "manifest.json")).pipe(
Effect.flatMap(decodeBuildManifest),
Effect.map(
(manifest) =>
new Set(
Object.values(manifest).flatMap((entry) => [
entry.file,
...(entry.css ?? []),
...(entry.assets ?? []),
]),
),
),
Effect.orElseSucceed(() => new Set<string>()),
);
});

const openStaticFile = Effect.fn("openStaticFile")(function* (filePath: string) {
const fileSystem = yield* FileSystem.FileSystem;
// Reject directories and special files before opening. Response metadata comes from the handle.
const pathInfo = yield* fileSystem.stat(filePath).pipe(Effect.orElseSucceed(() => null));
if (pathInfo?.type !== "File") return null;
const file = yield* fileSystem.open(filePath, { flag: "r" });
const info = yield* file.stat;
return info.type === "File" ? { file, info } : null;
});

const streamStaticFile = (file: FileSystem.File, size: bigint) =>
Stream.unfold(
0n,
Effect.fnUntraced(function* (offset: bigint) {
if (offset >= size) return;
const remaining = size - offset;
const bytes = yield* file.readAlloc(remaining < 65_536n ? remaining : 65_536n);
if (Option.isNone(bytes)) return;
return [bytes.value, offset + BigInt(bytes.value.byteLength)] as const;
}),
);

const handleStaticAndDevRequest = Effect.fn("handleStaticAndDevRequest")(
function* (immutableBuildAssets: ReadonlySet<string>) {
const request = yield* HttpServerRequest.HttpServerRequest;
const url = HttpServerRequest.toURL(request);

Expand All @@ -461,7 +518,6 @@ export const staticAndDevRouteLayer = HttpRouter.add(
});
}

const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const staticRoot = path.resolve(staticDir);
const staticRequestPath = url.value.pathname === "/" ? "/index.html" : url.value.pathname;
Expand Down Expand Up @@ -495,30 +551,78 @@ export const staticAndDevRouteLayer = HttpRouter.add(
}
}

const fileInfo = yield* fileSystem.stat(filePath).pipe(Effect.orElseSucceed(() => null));
if (!fileInfo || fileInfo.type !== "File") {
const indexPath = path.resolve(staticRoot, "index.html");
const indexData = yield* fileSystem
.readFile(indexPath)
.pipe(Effect.orElseSucceed(() => null));
if (!indexData) {
let opened = yield* openStaticFile(filePath);
if (!opened) {
filePath = path.resolve(staticRoot, "index.html");
opened = yield* openStaticFile(filePath);
if (!opened) {
return HttpServerResponse.text("Not Found", { status: 404 });
}
return HttpServerResponse.uint8Array(indexData, {
status: 200,
contentType: "text/html; charset=utf-8",
});
}
const fileInfo = opened.info;
const mimeType = Mime.getType(filePath) ?? "application/octet-stream";
const isHtml = mimeType === "text/html";

// A hash-like name is not enough: custom static files can use the same naming pattern.
const relativePath = path.relative(staticRoot, filePath).replaceAll("\\", "/");
const immutable =
!isHtml &&
/^assets\/.+-[\w-]{8}\.[^/]+$/.test(relativePath) &&
immutableBuildAssets.has(relativePath);
const headers: Record<string, string> = {
"Cache-Control": immutable ? "public, max-age=31536000, immutable" : "no-cache",
};
// Deployments can preserve HTML size and mtime while changing its bundle URLs.
const modifiedAt = isHtml ? undefined : Option.getOrUndefined(fileInfo.mtime);
const etag = modifiedAt
? `W/"${fileInfo.size.toString(16)}-${modifiedAt.getTime().toString(16)}"`
: undefined;
if (etag !== undefined && modifiedAt !== undefined) {
headers.ETag = etag;
headers["Last-Modified"] = modifiedAt.toUTCString();
}

const contentType = Mime.getType(filePath) ?? "application/octet-stream";
const data = yield* fileSystem.readFile(filePath).pipe(Effect.orElseSucceed(() => null));
if (!data) {
return HttpServerResponse.text("Internal Server Error", { status: 500 });
// If-None-Match takes precedence over dates and uses weak comparison for
// GET/HEAD, including when compression changes the transferred bytes.
const ifNoneMatch = request.headers["if-none-match"];
const ifModifiedSince = request.headers["if-modified-since"];
const unchanged =
ifNoneMatch !== undefined
? ifNoneMatch.split(",").some((value) => {
const candidate = value.trim();
return (
candidate === "*" ||
(etag !== undefined && candidate.replace(/^W\//i, "") === etag.slice(2))
);
})
: ifModifiedSince !== undefined &&
modifiedAt !== undefined &&
Date.parse(modifiedAt.toUTCString()) <= Date.parse(ifModifiedSince);
if (!isHtml && unchanged) {
return HttpServerResponse.empty({
status: 304,
headers: { ...headers, Vary: "Accept-Encoding" },
});
}

return HttpServerResponse.uint8Array(data, {
status: 200,
const contentType = isHtml ? "text/html; charset=utf-8" : mimeType;
// The request scope closes the handle for GET, HEAD, 304, errors, and cancellation.
// HEAD still passes through compression, which selects headers without reading the stream.
return HttpServerResponse.stream(streamStaticFile(opened.file, fileInfo.size), {
headers,
contentType,
contentLength: Number(fileInfo.size),
});
},
Effect.catchTags({
PlatformError: () =>
Effect.succeed(HttpServerResponse.text("Internal Server Error", { status: 500 })),
}),
);

// Read the installed build's manifest once. Unknown files use revalidation.
export const staticAndDevRouteLayer = Layer.unwrap(
loadImmutableBuildAssets.pipe(
Effect.map((assets) => HttpRouter.add("GET", "*", handleStaticAndDevRequest(assets))),
),
);
Loading
Loading