Skip to content

Commit 6064369

Browse files
authored
fix(javascript): move gzip compression to node-only builds, remove fflate (#6154)
1 parent 7714dac commit 6064369

File tree

13 files changed

+125
-76
lines changed

13 files changed

+125
-76
lines changed

.github/workflows/check.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,9 @@ jobs:
228228
run: cd clients/algoliasearch-client-javascript && yarn test
229229

230230
- name: Test JavaScript bundle size
231-
if: ${{ startsWith(github.head_ref, 'chore/prepare-release-') }}
232231
run: cd clients/algoliasearch-client-javascript && yarn test:size
233232

234233
- name: Test JavaScript bundle and types
235-
if: ${{ startsWith(github.head_ref, 'chore/prepare-release-') }}
236234
run: cd clients/algoliasearch-client-javascript && yarn test:bundle
237235

238236
- name: Remove previous CTS output

clients/algoliasearch-client-javascript/package.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,55 +27,55 @@
2727
"files": [
2828
{
2929
"path": "packages/algoliasearch/dist/algoliasearch.umd.js",
30-
"maxSize": "14.60KB"
30+
"maxSize": "14.8KB"
3131
},
3232
{
3333
"path": "packages/algoliasearch/dist/lite/builds/browser.umd.js",
34-
"maxSize": "3.95KB"
34+
"maxSize": "4.2KB"
3535
},
3636
{
3737
"path": "packages/abtesting/dist/builds/browser.umd.js",
38-
"maxSize": "4.35KB"
38+
"maxSize": "4.5KB"
3939
},
4040
{
4141
"path": "packages/client-abtesting/dist/builds/browser.umd.js",
42-
"maxSize": "4.20KB"
42+
"maxSize": "4.4KB"
4343
},
4444
{
4545
"path": "packages/client-analytics/dist/builds/browser.umd.js",
46-
"maxSize": "4.85KB"
46+
"maxSize": "5.1KB"
4747
},
4848
{
4949
"path": "packages/composition/dist/builds/browser.umd.js",
50-
"maxSize": "4.75KB"
50+
"maxSize": "5.0KB"
5151
},
5252
{
5353
"path": "packages/client-insights/dist/builds/browser.umd.js",
54-
"maxSize": "3.90KB"
54+
"maxSize": "4.2KB"
5555
},
5656
{
5757
"path": "packages/client-personalization/dist/builds/browser.umd.js",
58-
"maxSize": "4.05KB"
58+
"maxSize": "4.3KB"
5959
},
6060
{
6161
"path": "packages/client-query-suggestions/dist/builds/browser.umd.js",
62-
"maxSize": "4.05KB"
62+
"maxSize": "4.3KB"
6363
},
6464
{
6565
"path": "packages/client-search/dist/builds/browser.umd.js",
66-
"maxSize": "7.35KB"
66+
"maxSize": "7.7KB"
6767
},
6868
{
6969
"path": "packages/ingestion/dist/builds/browser.umd.js",
70-
"maxSize": "6.75KB"
70+
"maxSize": "7.1KB"
7171
},
7272
{
7373
"path": "packages/monitoring/dist/builds/browser.umd.js",
74-
"maxSize": "4.00KB"
74+
"maxSize": "4.3KB"
7575
},
7676
{
7777
"path": "packages/recommend/dist/builds/browser.umd.js",
78-
"maxSize": "4.15KB"
78+
"maxSize": "4.5KB"
7979
}
8080
]
8181
},

clients/algoliasearch-client-javascript/packages/client-common/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@
3434
"test": "tsc --noEmit && vitest --run",
3535
"test:bundle": "publint . && attw --pack ."
3636
},
37-
"dependencies": {
38-
"fflate": "0.8.2"
39-
},
37+
"dependencies": {},
4038
"devDependencies": {
4139
"@arethetypeswrong/cli": "0.18.2",
4240
"@types/node": "24.12.0",
Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,41 @@
1-
import { gunzipSync } from 'fflate';
2-
import { beforeEach, describe, expect, test } from 'vitest';
1+
import { gunzipSync, gzipSync } from 'node:zlib';
2+
import { beforeEach, describe, expect, test, vi } from 'vitest';
33
import { createMemoryCache, createNullCache } from '../../cache';
44
import { createNullLogger } from '../../logger';
55
import { createTransporter } from '../../transporter';
66
import { COMPRESSION_THRESHOLD } from '../../transporter/compress';
7-
import type { AlgoliaAgent, EndRequest } from '../../types';
7+
import type { AlgoliaAgent, EndRequest, Logger } from '../../types';
88

9-
// A payload large enough to exceed COMPRESSION_THRESHOLD
109
const largePayload = { data: 'x'.repeat(COMPRESSION_THRESHOLD + 1) };
1110

1211
const algoliaAgent: AlgoliaAgent = {
1312
value: 'test',
1413
add: () => algoliaAgent,
1514
};
1615

17-
function makeTransporter(compression?: 'gzip', onRequest?: (req: EndRequest) => void) {
16+
async function gzipCompress(data: string): Promise<Uint8Array> {
17+
return gzipSync(Buffer.from(data));
18+
}
19+
20+
function makeTransporter(opts: {
21+
compress?: (data: string) => Promise<Uint8Array>;
22+
compression?: 'gzip';
23+
logger?: Logger;
24+
onRequest?: (req: EndRequest) => void;
25+
}) {
1826
return createTransporter({
1927
hosts: [{ url: 'localhost', accept: 'readWrite', protocol: 'https' }],
2028
hostsCache: createNullCache(),
2129
baseHeaders: {},
2230
baseQueryParameters: {},
2331
algoliaAgent,
24-
logger: createNullLogger(),
32+
logger: opts.logger ?? createNullLogger(),
2533
timeouts: { connect: 1000, read: 2000, write: 3000 },
26-
...(compression ? { compression } : {}),
34+
...(opts.compress ? { compress: opts.compress } : {}),
35+
...(opts.compression ? { compression: opts.compression } : {}),
2736
requester: {
2837
send: async (req) => {
29-
onRequest?.(req);
38+
opts.onRequest?.(req);
3039
return { status: 200, content: '{}', isTimedOut: false };
3140
},
3241
},
@@ -42,28 +51,28 @@ describe('compression', () => {
4251
captured = undefined;
4352
});
4453

45-
test('does not compress when compression is not configured', async () => {
46-
const transporter = makeTransporter(undefined, (req) => {
47-
captured = req;
54+
test('does not compress when compression is not enabled', async () => {
55+
const transporter = makeTransporter({
56+
compress: gzipCompress,
57+
onRequest: (req) => {
58+
captured = req;
59+
},
4860
});
4961

50-
await transporter.request({
51-
method: 'POST',
52-
path: '/test',
53-
queryParameters: {},
54-
headers: {},
55-
data: { foo: 'bar' },
56-
});
62+
await transporter.request({ method: 'POST', path: '/test', queryParameters: {}, headers: {}, data: largePayload });
5763

5864
expect(captured).toBeDefined();
5965
expect(captured!.headers['content-encoding']).toBeUndefined();
6066
expect(typeof captured!.data).toBe('string');
61-
expect(captured!.data).toBe('{"foo":"bar"}');
6267
});
6368

64-
test('compresses POST body when compression is gzip', async () => {
65-
const transporter = makeTransporter('gzip', (req) => {
66-
captured = req;
69+
test('compresses POST body when compression is gzip and compress is provided', async () => {
70+
const transporter = makeTransporter({
71+
compress: gzipCompress,
72+
compression: 'gzip',
73+
onRequest: (req) => {
74+
captured = req;
75+
},
6776
});
6877

6978
await transporter.request({ method: 'POST', path: '/test', queryParameters: {}, headers: {}, data: largePayload });
@@ -72,13 +81,17 @@ describe('compression', () => {
7281
expect(captured!.headers['content-encoding']).toBe('gzip');
7382
expect(captured!.data).toBeInstanceOf(Uint8Array);
7483

75-
const decompressed = new TextDecoder().decode(gunzipSync(captured!.data as Uint8Array));
84+
const decompressed = gunzipSync(Buffer.from(captured!.data as Uint8Array)).toString();
7685
expect(decompressed).toBe(JSON.stringify(largePayload));
7786
});
7887

79-
test('compresses PUT body when compression is gzip', async () => {
80-
const transporter = makeTransporter('gzip', (req) => {
81-
captured = req;
88+
test('compresses PUT body when compression is gzip and compress is provided', async () => {
89+
const transporter = makeTransporter({
90+
compress: gzipCompress,
91+
compression: 'gzip',
92+
onRequest: (req) => {
93+
captured = req;
94+
},
8295
});
8396

8497
await transporter.request({ method: 'PUT', path: '/test', queryParameters: {}, headers: {}, data: largePayload });
@@ -87,13 +100,17 @@ describe('compression', () => {
87100
expect(captured!.headers['content-encoding']).toBe('gzip');
88101
expect(captured!.data).toBeInstanceOf(Uint8Array);
89102

90-
const decompressed = new TextDecoder().decode(gunzipSync(captured!.data as Uint8Array));
103+
const decompressed = gunzipSync(Buffer.from(captured!.data as Uint8Array)).toString();
91104
expect(decompressed).toBe(JSON.stringify(largePayload));
92105
});
93106

94107
test('does not compress POST when body is below threshold', async () => {
95-
const transporter = makeTransporter('gzip', (req) => {
96-
captured = req;
108+
const transporter = makeTransporter({
109+
compress: gzipCompress,
110+
compression: 'gzip',
111+
onRequest: (req) => {
112+
captured = req;
113+
},
97114
});
98115

99116
await transporter.request({
@@ -110,8 +127,12 @@ describe('compression', () => {
110127
});
111128

112129
test('does not compress GET requests', async () => {
113-
const transporter = makeTransporter('gzip', (req) => {
114-
captured = req;
130+
const transporter = makeTransporter({
131+
compress: gzipCompress,
132+
compression: 'gzip',
133+
onRequest: (req) => {
134+
captured = req;
135+
},
115136
});
116137

117138
await transporter.request({ method: 'GET', path: '/test', queryParameters: {}, headers: {} });
@@ -121,15 +142,36 @@ describe('compression', () => {
121142
expect(captured!.data).toBeUndefined();
122143
});
123144

124-
test('does not compress POST when body is empty', async () => {
125-
const transporter = makeTransporter('gzip', (req) => {
126-
captured = req;
145+
test('logs warning when compression is gzip but compress method is missing', async () => {
146+
const logger: Logger = { debug: vi.fn(), info: vi.fn(), error: vi.fn() };
147+
const transporter = makeTransporter({
148+
compression: 'gzip',
149+
logger,
150+
onRequest: (req) => {
151+
captured = req;
152+
},
127153
});
128154

129-
await transporter.request({ method: 'POST', path: '/test', queryParameters: {}, headers: {} });
155+
await transporter.request({ method: 'POST', path: '/test', queryParameters: {}, headers: {}, data: largePayload });
130156

131157
expect(captured).toBeDefined();
132158
expect(captured!.headers['content-encoding']).toBeUndefined();
133-
expect(captured!.data).toBeUndefined();
159+
expect(typeof captured!.data).toBe('string');
160+
expect(logger.info).toHaveBeenCalledWith('Compression is disabled because no compress method is available.');
161+
});
162+
163+
test('silently sends uncompressed when compression is gzip, compress is missing, and null logger', async () => {
164+
const transporter = makeTransporter({
165+
compression: 'gzip',
166+
onRequest: (req) => {
167+
captured = req;
168+
},
169+
});
170+
171+
await transporter.request({ method: 'POST', path: '/test', queryParameters: {}, headers: {}, data: largePayload });
172+
173+
expect(captured).toBeDefined();
174+
expect(captured!.headers['content-encoding']).toBeUndefined();
175+
expect(typeof captured!.data).toBe('string');
134176
});
135177
});
Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1 @@
1-
import { gzipSync } from 'fflate';
2-
31
export const COMPRESSION_THRESHOLD = 750;
4-
5-
/**
6-
* Compresses a string using gzip via fflate.
7-
* Works in both Node.js and browsers with no platform-specific code.
8-
*/
9-
export function compress(data: string): Uint8Array {
10-
return gzipSync(new TextEncoder().encode(data));
11-
}

clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
Transporter,
1010
TransporterOptions,
1111
} from '../types';
12-
import { compress, COMPRESSION_THRESHOLD } from './compress';
12+
import { COMPRESSION_THRESHOLD } from './compress';
1313
import { createStatefulHost } from './createStatefulHost';
1414
import { RetryError } from './errors';
1515
import { deserializeFailure, deserializeSuccess, serializeData, serializeHeaders, serializeUrl } from './helpers';
@@ -32,6 +32,7 @@ export function createTransporter({
3232
requester,
3333
requestsCache,
3434
responsesCache,
35+
compress,
3536
compression,
3637
}: TransporterOptions): Transporter {
3738
async function createRetryableOptions(compatibleHosts: Host[]): Promise<RetryableOptions> {
@@ -84,13 +85,18 @@ export function createTransporter({
8485
const serializedData = serializeData(request, requestOptions);
8586
const headers = serializeHeaders(baseHeaders, request.headers, requestOptions.headers);
8687

87-
const shouldCompress =
88+
const wantsCompression =
8889
compression === 'gzip' &&
8990
serializedData !== undefined &&
9091
serializedData.length > COMPRESSION_THRESHOLD &&
9192
(request.method === 'POST' || request.method === 'PUT');
9293

93-
const data = shouldCompress ? compress(serializedData) : serializedData;
94+
if (wantsCompression && compress === undefined) {
95+
logger.info('Compression is disabled because no compress method is available.');
96+
}
97+
98+
const shouldCompress = wantsCompression && compress !== undefined;
99+
const data = shouldCompress ? await compress(serializedData) : serializedData;
94100
if (shouldCompress) {
95101
headers['content-encoding'] = 'gzip';
96102
}

clients/algoliasearch-client-javascript/packages/client-common/src/types/createClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ export type CreateClientOptions = Omit<TransporterOptions, OverriddenTransporter
1212
algoliaAgents: AlgoliaAgentOptions[];
1313
};
1414

15-
export type ClientOptions = Partial<Omit<CreateClientOptions, 'apiKey' | 'appId'>>;
15+
export type ClientOptions = Partial<Omit<CreateClientOptions, 'apiKey' | 'appId' | 'compress'>>;

clients/algoliasearch-client-javascript/packages/client-common/src/types/transporter.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,13 @@ export type TransporterOptions = {
152152
algoliaAgent: AlgoliaAgent;
153153

154154
/**
155-
* The compression algorithm to use when sending POST/PUT request bodies.
156-
* When set to `'gzip'`, request bodies are gzip-compressed and
157-
* `Content-Encoding: gzip` is added to the headers.
158-
* Works on all Node.js versions and browsers with no native API requirements.
155+
* An optional function to compress request bodies before sending.
156+
* When provided, POST/PUT bodies exceeding the compression threshold
157+
* will be compressed and `Content-Encoding: gzip` is added to the headers.
158+
* Node builds use node:zlib, browser/worker builds use CompressionStream when available.
159159
*/
160+
compress?: (data: string) => Promise<Uint8Array>;
161+
160162
compression?: 'gzip';
161163
};
162164

templates/javascript/clients/client/builds/browser.mustache

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// {{{generationBanner}}}
22

33
{{> client/builds/definition}}
4+
const { compression: _compression, ...browserOptions } = options || {};
5+
46
return create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}({
57
appId,
68
apiKey,{{#hasRegionalHost}}region,{{/hasRegionalHost}}
@@ -21,7 +23,7 @@
2123
createMemoryCache(),
2224
],
2325
}),
24-
...options,
26+
...browserOptions,
2527
});
2628
}
2729

templates/javascript/clients/client/builds/fetch.mustache

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
export type {{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}} = ReturnType<typeof create{{#lambda.titlecase}}{{clientName}}{{/lambda.titlecase}}>{{#searchHelpers}} & SearchClientNodeHelpers{{/searchHelpers}};
44

5+
import { gzipSync } from 'node:zlib';
6+
57
{{#searchHelpers}}
68
import { createHmac } from 'node:crypto';
79
{{/searchHelpers}}
@@ -22,6 +24,7 @@ import { createHmac } from 'node:crypto';
2224
responsesCache: createNullCache(),
2325
requestsCache: createNullCache(),
2426
hostsCache: createMemoryCache(),
27+
compress: async (data: string): Promise<Uint8Array> => gzipSync(Buffer.from(data)),
2528
...options,
2629
}),
2730
{{#searchHelpers}}

0 commit comments

Comments
 (0)