Skip to content

Commit aac2962

Browse files
authored
Merge pull request #262 from gadget-inc/smaller-bundle
Smaller bundle size
2 parents 5c41db7 + 175e121 commit aac2962

File tree

10 files changed

+255
-285
lines changed

10 files changed

+255
-285
lines changed

Contributing.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ It can be annoying to work with these packages via `pnpm link` sometimes, so we
1616
- push that to the remote git repo
1717
- and log out a version you can then refer to from other repos
1818

19+
# Checking test bundle sizes
20+
21+
We have a small project setup for evaluating what the bundled size of these dependencies might be together. Run:
22+
23+
```shell
24+
pnpm -F=test-bundles test-build
25+
```
26+
27+
to build the test bundles
28+
1929
# Releasing
2030

2131
Releasing is done automatically via [our release workflow](.github/workflows/release.yml). Any commits to the main branch that changes one of our `packages/**/package.json` versions will automatically be published.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import fs from "fs/promises";
2+
import globby from "globby";
3+
import path from "path";
4+
import { fileURLToPath } from "url";
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
8+
9+
// replace version constant strings with the version we're about to release
10+
11+
const version = JSON.parse(await fs.readFile(path.join(__dirname, "..", "package.json"), "utf-8")).version;
12+
13+
for (const file of await globby(path.join(__dirname, "..", "dist"), { onlyFiles: true })) {
14+
const content = await fs.readFile(file, "utf-8");
15+
if (content.includes("<prerelease>")) {
16+
await fs.writeFile(file, content.replace("<prerelease>", version));
17+
}
18+
}

packages/api-client-core/package.json

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,35 +19,34 @@
1919
},
2020
"source": "src/index.ts",
2121
"main": "dist/cjs/index.js",
22+
"sideEffects": false,
2223
"scripts": {
2324
"typecheck": "tsc --noEmit",
24-
"build": "rm -rf dist && tsc -b tsconfig.cjs.json tsconfig.esm.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json && echo '{\"type\": \"module\"}' > dist/esm/package.json",
25+
"build": "rm -rf dist && tsc -b tsconfig.cjs.json tsconfig.esm.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json && echo '{\"type\": \"module\"}' > dist/esm/package.json && node --loader ts-node/esm build/replace-version-constant.ts",
2526
"watch": "rm -rf dist && tsc --watch --preserveWatchOutput",
2627
"prepublishOnly": "pnpm build",
2728
"prerelease": "gitpkg publish"
2829
},
2930
"dependencies": {
30-
"@opentelemetry/api": "^1.4.0",
3131
"@urql/core": "^4.0.10",
3232
"cross-fetch": "^3.1.5",
33-
"tiny-graphql-query-compiler": "^0.2.2",
3433
"graphql": "~16.6.0",
3534
"graphql-ws": "^5.13.1",
3635
"isomorphic-ws": "^5.0.0",
37-
"lodash.clonedeep": "^4.5.0",
38-
"lodash.isequal": "^4.5.0",
36+
"klona": "^2.0.6",
37+
"tiny-graphql-query-compiler": "^0.2.2",
3938
"ws": "^8.13.0"
4039
},
4140
"devDependencies": {
42-
"tiny-graphql-query-compiler": "*",
43-
"@types/lodash.clonedeep": "^4.5.6",
44-
"@types/lodash.isequal": "^4.5.5",
4541
"@types/node": "^16.11.7",
4642
"conditional-type-checks": "^1.0.6",
43+
"globby": "^11.0.4",
4744
"gql-tag": "^1.0.1",
45+
"nock": "^13.3.1",
4846
"react": "^18.2.0",
4947
"react-dom": "^18.2.0",
50-
"nock": "^13.3.1",
48+
"tiny-graphql-query-compiler": "workspace:*",
49+
"ts-node": "^10.9.1",
5150
"type-fest": "^3.3.0",
5251
"typescript": "5.0.4"
5352
}

packages/api-client-core/src/GadgetConnection.ts

Lines changed: 91 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ClientOptions, RequestPolicy } from "@urql/core";
22
import { Client, cacheExchange, fetchExchange, subscriptionExchange } from "@urql/core";
3-
import fetchPolyfill from "cross-fetch";
3+
44
import type { ExecutionResult } from "graphql";
55
import type { Sink, Client as SubscriptionClient, ClientOptions as SubscriptionClientOptions } from "graphql-ws";
66
import { CloseCode, createClient as createSubscriptionClient } from "graphql-ws";
@@ -11,16 +11,8 @@ import { GadgetTransaction, TransactionRolledBack } from "./GadgetTransaction.js
1111
import type { BrowserStorage } from "./InMemoryStorage.js";
1212
import { InMemoryStorage } from "./InMemoryStorage.js";
1313
import { operationNameExchange } from "./exchanges/operationNameExchange.js";
14-
import { otelExchange } from "./exchanges/otelExchange.js";
1514
import { urlParamExchange } from "./exchanges/urlParamExchange.js";
16-
import {
17-
GadgetUnexpectedCloseError,
18-
GadgetWebsocketConnectionTimeoutError,
19-
getCurrentSpan,
20-
isCloseEvent,
21-
storageAvailable,
22-
traceFunction,
23-
} from "./support.js";
15+
import { GadgetUnexpectedCloseError, GadgetWebsocketConnectionTimeoutError, isCloseEvent, storageAvailable } from "./support.js";
2416

2517
export type TransactionRun<T> = (transaction: GadgetTransaction) => Promise<T>;
2618
export interface GadgetSubscriptionClientOptions extends Partial<SubscriptionClientOptions> {
@@ -47,7 +39,7 @@ export interface GadgetConnectionOptions {
4739
websocketsEndpoint?: string;
4840
subscriptionClientOptions?: GadgetSubscriptionClientOptions;
4941
websocketImplementation?: any;
50-
fetchImplementation?: typeof fetchPolyfill;
42+
fetchImplementation?: typeof globalThis.fetch;
5143
environment?: "Development" | "Production";
5244
requestPolicy?: ClientOptions["requestPolicy"];
5345
applicationId?: string;
@@ -74,12 +66,14 @@ export enum AuthenticationMode {
7466
* Manages transactions and the connection to a Gadget API
7567
*/
7668
export class GadgetConnection {
69+
version = "<prerelease>" as const;
70+
7771
// Options used when generating new GraphQL clients for the base connection and for for transactions
7872
private endpoint: string;
7973
private subscriptionClientOptions?: SubscriptionClientOptions;
8074
private websocketsEndpoint: string;
8175
private websocketImplementation: any;
82-
private _fetchImplementation: typeof fetchPolyfill;
76+
private _fetchImplementation: typeof globalThis.fetch;
8377
private environment: "Development" | "Production";
8478
private exchanges: Required<Exchanges>;
8579

@@ -103,7 +97,10 @@ export class GadgetConnection {
10397
} else if (typeof window != "undefined" && window.fetch) {
10498
this._fetchImplementation = window.fetch.bind(window);
10599
} else {
106-
this._fetchImplementation = fetchPolyfill;
100+
this._fetchImplementation = async (...args: [any]) => {
101+
const { fetch } = await import("cross-fetch");
102+
return await fetch(...args);
103+
};
107104
}
108105
this.websocketImplementation = options.websocketImplementation ?? globalThis?.WebSocket ?? WebSocket;
109106
this.websocketsEndpoint = options.websocketsEndpoint ?? options.endpoint + "/batch";
@@ -132,7 +129,7 @@ export class GadgetConnection {
132129
return this.currentTransaction?.client || this.baseClient;
133130
}
134131

135-
set fetchImplementation(implementation: typeof fetchPolyfill) {
132+
set fetchImplementation(implementation: typeof globalThis.fetch) {
136133
this._fetchImplementation = implementation;
137134
this.resetClients();
138135
}
@@ -182,96 +179,90 @@ export class GadgetConnection {
182179
transaction: {
183180
<T>(options: GadgetSubscriptionClientOptions, run: TransactionRun<T>): Promise<T>;
184181
<T>(run: TransactionRun<T>): Promise<T>;
185-
} = traceFunction(
186-
"api-client.transaction",
187-
async <T>(optionsOrRun: GadgetSubscriptionClientOptions | TransactionRun<T>, maybeRun?: TransactionRun<T>): Promise<T> => {
188-
let run: TransactionRun<T>;
189-
let options: GadgetSubscriptionClientOptions;
190-
191-
if (maybeRun) {
192-
run = maybeRun;
193-
options = optionsOrRun as GadgetSubscriptionClientOptions;
194-
} else {
195-
run = optionsOrRun as TransactionRun<T>;
196-
options = {};
197-
}
182+
} = async <T>(optionsOrRun: GadgetSubscriptionClientOptions | TransactionRun<T>, maybeRun?: TransactionRun<T>): Promise<T> => {
183+
let run: TransactionRun<T>;
184+
let options: GadgetSubscriptionClientOptions;
198185

199-
if (this.currentTransaction) {
200-
return await run(this.currentTransaction);
201-
}
186+
if (maybeRun) {
187+
run = maybeRun;
188+
options = optionsOrRun as GadgetSubscriptionClientOptions;
189+
} else {
190+
run = optionsOrRun as TransactionRun<T>;
191+
options = {};
192+
}
202193

203-
getCurrentSpan()?.setAttributes({ applicationId: this.options.applicationId, environmentName: this.environment });
194+
if (this.currentTransaction) {
195+
return await run(this.currentTransaction);
196+
}
204197

205-
let subscriptionClient: SubscriptionClient | null = null;
206-
let transaction;
198+
let subscriptionClient: SubscriptionClient | null = null;
199+
let transaction;
200+
try {
201+
// The server will error if it receives any operations before the auth dance has been completed, so we block on that happening before sending our first operation. It's important that this happens synchronously after instantiating the client so we don't miss any messages
202+
subscriptionClient = await this.waitForOpenedConnection({
203+
isFatalConnectionProblem(errorOrCloseEvent) {
204+
// any interruption of the transaction is fatal to the transaction
205+
console.warn("Transport error encountered during transaction processing", errorOrCloseEvent);
206+
return true;
207+
},
208+
connectionAckWaitTimeout: DEFAULT_CONN_ACK_TIMEOUT,
209+
...options,
210+
lazy: false,
211+
// super ultra critical option that ensures graphql-ws doesn't automatically close the websocket connection when there are no outstanding operations. this is key so we can start a transaction then make mutations within it
212+
lazyCloseTimeout: 100000,
213+
retryAttempts: 0,
214+
});
215+
216+
const client = new Client({
217+
url: "/-", // not used because there's no fetch exchange, set for clarity
218+
requestPolicy: "network-only", // skip any cached data during transactions
219+
exchanges: [
220+
...this.exchanges.beforeAll,
221+
operationNameExchange,
222+
...this.exchanges.beforeAsync,
223+
subscriptionExchange({
224+
forwardSubscription(request) {
225+
const input = { ...request, query: request.query || "" };
226+
return {
227+
subscribe: (sink) => {
228+
const dispose = subscriptionClient!.subscribe(input, sink as Sink<ExecutionResult>);
229+
return {
230+
unsubscribe: dispose,
231+
};
232+
},
233+
};
234+
},
235+
enableAllOperations: true,
236+
}),
237+
...this.exchanges.afterAll,
238+
],
239+
});
240+
(client as any)[$gadgetConnection] = this;
241+
242+
transaction = new GadgetTransaction(client, subscriptionClient);
243+
this.currentTransaction = transaction;
244+
await transaction.start();
245+
const result = await run(transaction);
246+
await transaction.commit();
247+
return result;
248+
} catch (error) {
207249
try {
208-
// The server will error if it receives any operations before the auth dance has been completed, so we block on that happening before sending our first operation. It's important that this happens synchronously after instantiating the client so we don't miss any messages
209-
subscriptionClient = await this.waitForOpenedConnection({
210-
isFatalConnectionProblem(errorOrCloseEvent) {
211-
// any interruption of the transaction is fatal to the transaction
212-
console.warn("Transport error encountered during transaction processing", errorOrCloseEvent);
213-
return true;
214-
},
215-
connectionAckWaitTimeout: DEFAULT_CONN_ACK_TIMEOUT,
216-
...options,
217-
lazy: false,
218-
// super ultra critical option that ensures graphql-ws doesn't automatically close the websocket connection when there are no outstanding operations. this is key so we can start a transaction then make mutations within it
219-
lazyCloseTimeout: 100000,
220-
retryAttempts: 0,
221-
});
222-
223-
const client = new Client({
224-
url: "/-", // not used because there's no fetch exchange, set for clarity
225-
requestPolicy: "network-only", // skip any cached data during transactions
226-
exchanges: [
227-
...this.exchanges.beforeAll,
228-
operationNameExchange,
229-
otelExchange,
230-
...this.exchanges.beforeAsync,
231-
subscriptionExchange({
232-
forwardSubscription(request) {
233-
const input = { ...request, query: request.query || "" };
234-
return {
235-
subscribe: (sink) => {
236-
const dispose = subscriptionClient!.subscribe(input, sink as Sink<ExecutionResult>);
237-
return {
238-
unsubscribe: dispose,
239-
};
240-
},
241-
};
242-
},
243-
enableAllOperations: true,
244-
}),
245-
...this.exchanges.afterAll,
246-
],
247-
});
248-
(client as any)[$gadgetConnection] = this;
249-
250-
transaction = new GadgetTransaction(client, subscriptionClient);
251-
this.currentTransaction = transaction;
252-
await transaction.start();
253-
const result = await run(transaction);
254-
await transaction.commit();
255-
return result;
256-
} catch (error) {
257-
try {
258-
if (transaction?.open) await transaction.rollback();
259-
} catch (rollbackError) {
260-
if (!(rollbackError instanceof TransactionRolledBack)) {
261-
console.warn("Encountered another error while rolling back a Gadget transaction that errored. The other error:", rollbackError);
262-
}
263-
}
264-
if (isCloseEvent(error)) {
265-
throw new GadgetUnexpectedCloseError(error);
266-
} else {
267-
throw error;
250+
if (transaction?.open) await transaction.rollback();
251+
} catch (rollbackError) {
252+
if (!(rollbackError instanceof TransactionRolledBack)) {
253+
console.warn("Encountered another error while rolling back a Gadget transaction that errored. The other error:", rollbackError);
268254
}
269-
} finally {
270-
await subscriptionClient?.dispose();
271-
this.currentTransaction = null;
272255
}
256+
if (isCloseEvent(error)) {
257+
throw new GadgetUnexpectedCloseError(error);
258+
} else {
259+
throw error;
260+
}
261+
} finally {
262+
await subscriptionClient?.dispose();
263+
this.currentTransaction = null;
273264
}
274-
);
265+
};
275266

276267
close() {
277268
this.disposeClient(this.baseSubscriptionClient);
@@ -290,7 +281,7 @@ export class GadgetConnection {
290281
* // fetch a relative URL from the endpoint this API client is configured to fetch from
291282
* await api.connection.fetch("/foo/bar");
292283
**/
293-
fetch = traceFunction("api-client.fetch", async (input: RequestInfo | URL, init: RequestInit = {}) => {
284+
fetch = async (input: RequestInfo | URL, init: RequestInit = {}) => {
294285
input = processMaybeRelativeInput(input, this.options.baseRouteURL ?? this.options.endpoint);
295286

296287
if (this.isGadgetRequest(input)) {
@@ -311,7 +302,7 @@ export class GadgetConnection {
311302
}
312303

313304
return response;
314-
});
305+
};
315306

316307
private isGadgetRequest(input: RequestInfo | URL) {
317308
let requestUrl;
@@ -345,7 +336,7 @@ export class GadgetConnection {
345336
}
346337

347338
private newBaseClient() {
348-
const exchanges = [...this.exchanges.beforeAll, operationNameExchange, otelExchange, urlParamExchange];
339+
const exchanges = [...this.exchanges.beforeAll, operationNameExchange, urlParamExchange];
349340

350341
// apply urql's default caching behaviour when client side (but skip it server side)
351342
if (typeof window != "undefined") {

packages/api-client-core/src/GadgetRecord.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import cloneDeep from "lodash.clonedeep";
2-
import isEqual from "lodash.isequal";
1+
import { klona as cloneDeep } from "klona";
32
import type { Jsonify } from "type-fest";
4-
import { toPrimitiveObject } from "./support.js";
3+
import { isEqual, toPrimitiveObject } from "./support.js";
54

65
export enum ChangeTracking {
76
SinceLoaded,

packages/api-client-core/src/exchanges/otelExchange.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)