Skip to content

Commit a118943

Browse files
feat(cli): add gzip compression to v2 docs publish requests
Co-Authored-By: cbro <cbro@buildwithfern.com>
1 parent 69d0414 commit a118943

3 files changed

Lines changed: 77 additions & 12 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- summary: |
2+
Gzip-compress v2 docs publish request bodies (register, finish, API
3+
registration, translation registration) to reduce transfer time for
4+
large deployments.
5+
type: feat
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Returns a `fetch` function that gzip-compresses request bodies using the
3+
* Web Streams `CompressionStream` API.
4+
*
5+
* Designed to be captured by oRPC's `LinkFetchClient` at construction time
6+
* so all subsequent requests through the client are transparently compressed.
7+
*
8+
* The FDR server registers `@fastify/compress` which transparently
9+
* decompresses incoming `Content-Encoding: gzip` request bodies.
10+
*/
11+
export function createGzipFetch(): typeof globalThis.fetch {
12+
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
13+
if (input instanceof Request && input.body != null) {
14+
const headers = new Headers(input.headers);
15+
headers.set("Content-Encoding", "gzip");
16+
headers.delete("Content-Length");
17+
18+
const compressedBody = input.body.pipeThrough(new CompressionStream("gzip"));
19+
20+
const compressedRequest = new Request(input.url, {
21+
method: input.method,
22+
headers,
23+
body: compressedBody,
24+
// @ts-expect-error duplex required for streaming request bodies in Node.js
25+
duplex: "half"
26+
});
27+
return globalThis.fetch(compressedRequest, init);
28+
}
29+
30+
return globalThis.fetch(input, init);
31+
};
32+
}
33+
34+
/**
35+
* Gzip-compresses a JSON body and returns a `RequestInit` suitable for
36+
* `fetch()`, with the correct `Content-Encoding` and `Content-Type` headers.
37+
*/
38+
export async function gzipJsonBody(body: unknown): Promise<{ body: ReadableStream; headers: Record<string, string> }> {
39+
const json = JSON.stringify(body);
40+
const stream = new Blob([json]).stream().pipeThrough(new CompressionStream("gzip"));
41+
return {
42+
body: stream,
43+
headers: {
44+
"Content-Encoding": "gzip",
45+
"Content-Type": "application/json"
46+
}
47+
};
48+
}

packages/cli/generation/remote-generation/remote-workspace-runner/src/publishDocs.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import * as mime from "mime-types";
2929
import { basename } from "path";
3030
import terminalLink from "terminal-link";
3131
import { buildTranslatedDocsDefinition } from "./buildTranslatedDocsDefinition.js";
32+
import { createGzipFetch, gzipJsonBody } from "./compressedFetch.js";
3233
import { getDocsDeployMode } from "./docsDeployMode.js";
3334
import { getDynamicGeneratorConfig } from "./getDynamicGeneratorConfig.js";
3435
import { measureImageSizes } from "./measureImageSizes.js";
@@ -209,10 +210,17 @@ export async function publishDocs({
209210
context.logger.info(`Docs deploy mode: ${deployMode}`);
210211
}
211212

213+
// Capture a gzip-compressing fetch into the oRPC client so all FDR
214+
// requests with a body are transparently compressed. LinkFetchClient
215+
// snapshots globalThis.fetch at construction time, so we swap it in
216+
// before building the client and restore immediately after.
217+
const savedFetch = globalThis.fetch;
218+
globalThis.fetch = createGzipFetch();
212219
const fdr = createFdrService({
213220
token: token.value,
214221
...(Object.keys(headers).length > 0 && { headers })
215222
});
223+
globalThis.fetch = savedFetch;
216224
const authConfig = { type: "public" as const };
217225

218226
if (excludeApis) {
@@ -883,24 +891,28 @@ export async function publishDocs({
883891
);
884892
// Use a raw fetch instead of the oRPC client to send `docsDefinition`
885893
// (the live server expects that field; the published fdr-sdk still uses `content`).
894+
const translationPayload = {
895+
domain: translationDomain,
896+
// Send customDomains in production so FDR fans the translation
897+
// S3 write out across every URL the docs are published to.
898+
// Skipped in preview because preview deploys to a single
899+
// ephemeral URL with no custom-domain mirrors.
900+
customDomains: preview ? [] : customDomains,
901+
orgId: organization,
902+
locale,
903+
docsDefinition: translatedDefinition
904+
};
905+
const compressed = await gzipJsonBody(translationPayload);
886906
const translationResponse = await fetch(`${fdrOrigin}/v2/registry/docs/translations/register`, {
887907
method: "POST",
888908
headers: {
889-
"Content-Type": "application/json",
909+
...compressed.headers,
890910
Authorization: `Bearer ${token.value}`,
891911
...headers // Include telemetry headers (X-CLI-Version, X-CI-Source, etc.)
892912
},
893-
body: JSON.stringify({
894-
domain: translationDomain,
895-
// Send customDomains in production so FDR fans the translation
896-
// S3 write out across every URL the docs are published to.
897-
// Skipped in preview because preview deploys to a single
898-
// ephemeral URL with no custom-domain mirrors.
899-
customDomains: preview ? [] : customDomains,
900-
orgId: organization,
901-
locale,
902-
docsDefinition: translatedDefinition
903-
})
913+
body: compressed.body,
914+
// @ts-expect-error duplex required for streaming request bodies in Node.js
915+
duplex: "half"
904916
});
905917
if (!translationResponse.ok) {
906918
const body = await translationResponse.text();

0 commit comments

Comments
 (0)