Skip to content

Commit f291302

Browse files
committed
Add support for running Gadget clients in environments without a Websocket implementation
This allows use in places like Shopify's locked down extension environment, where we do have a `globalThis.fetch`, but don't have a `globalThis.WebSocket`. That's ok -- extensions don't really need to open up transactions against Gadget and so don't need websockets. In a subscriptions/livequery-y future that'd change, but investing in a whole other transport is a lot of work. So, let's at least unblock using the client for normal reads and writes!
1 parent fe596df commit f291302

File tree

3 files changed

+47
-6
lines changed

3 files changed

+47
-6
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* @jest-environment ./spec/remote-ui-environment.ts
3+
*/
4+
jest.mock("isomorphic-ws", () => null); // mimic browser environment for remote-ui where the websocket global is not available
5+
import { GadgetConnection } from "../src/GadgetConnection.js";
6+
import { GadgetConnectionSharedSuite } from "./GadgetConnection-suite.js";
7+
8+
describe("GadgetConnection in remote-ui", () => {
9+
GadgetConnectionSharedSuite();
10+
11+
test("throws an error concerning missing websocket constructor as it is not available", async () => {
12+
const connection = new GadgetConnection({ endpoint: "https://someapp.gadget.app" });
13+
await expect(async () => await connection.transaction({}, async () => true)).rejects.toThrowErrorMatchingInlineSnapshot(
14+
`"Can't use this GadgetClient for this subscription-based operation as there's no global WebSocket implementation available. Please pass one as the \`websocketImplementation\` option to the GadgetClient constructor."`
15+
);
16+
});
17+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { TestEnvironment } from "jest-environment-node";
2+
3+
/**
4+
* Implements an environment with only the stuff present in a remote-ui environment
5+
* What that is isn't actually documented anywhere as best I can tell, but we do know there is no WebSocket global
6+
* See https://github.com/Shopify/remote-ui */
7+
export default class RemoteUIEnvironment extends TestEnvironment {
8+
async setup() {
9+
await super.setup();
10+
delete (this.global as any).WebSocket;
11+
}
12+
}

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,14 @@ export class GadgetConnection {
7373
private endpoint: string;
7474
private subscriptionClientOptions?: SubscriptionClientOptions;
7575
private websocketsEndpoint: string;
76-
private websocketImplementation: any;
76+
private websocketImplementation?: WebSocket;
7777
private _fetchImplementation: typeof globalThis.fetch;
7878
private environment: "Development" | "Production";
7979
private exchanges: Required<Exchanges>;
8080

8181
// the base client using HTTP requests that non-transactional operations will use
8282
private baseClient: Client;
83-
private baseSubscriptionClient: SubscriptionClient;
83+
private baseSubscriptionClient?: SubscriptionClient;
8484

8585
// the transactional websocket client that will be used inside a transaction block
8686
private currentTransaction: GadgetTransaction | null = null;
@@ -117,8 +117,6 @@ export class GadgetConnection {
117117

118118
this.setAuthenticationMode(options.authenticationMode);
119119

120-
// the base client for subscriptions is lazy so we don't open unnecessary connections to the backend, and it reconnects to deal with network issues
121-
this.baseSubscriptionClient = this.newSubscriptionClient({ lazy: true });
122120
this.baseClient = this.newBaseClient();
123121
}
124122

@@ -266,7 +264,7 @@ export class GadgetConnection {
266264
};
267265

268266
close() {
269-
this.disposeClient(this.baseSubscriptionClient);
267+
if (this.baseSubscriptionClient) this.disposeClient(this.baseSubscriptionClient);
270268
if (this.currentTransaction) {
271269
this.currentTransaction.close();
272270
}
@@ -352,7 +350,8 @@ export class GadgetConnection {
352350
return {
353351
subscribe: (sink) => {
354352
const input = { ...request, query: request.query || "" };
355-
const dispose = this.baseSubscriptionClient.subscribe(input, sink as Sink<ExecutionResult>);
353+
354+
const dispose = this.getBaseSubscriptionClient().subscribe(input, sink as Sink<ExecutionResult>);
356355
return {
357356
unsubscribe: dispose,
358357
};
@@ -374,6 +373,12 @@ export class GadgetConnection {
374373
}
375374

376375
private newSubscriptionClient(overrides: GadgetSubscriptionClientOptions) {
376+
if (!this.websocketImplementation) {
377+
throw new Error(
378+
"Can't use this GadgetClient for this subscription-based operation as there's no global WebSocket implementation available. Please pass one as the `websocketImplementation` option to the GadgetClient constructor."
379+
);
380+
}
381+
377382
let url = this.websocketsEndpoint;
378383
if (overrides?.urlParams) {
379384
const params = new URLSearchParams();
@@ -503,6 +508,13 @@ export class GadgetConnection {
503508
maybePromise.catch((err: any) => console.error(`Error closing SubscriptionClient: ${err.message}`));
504509
}
505510
}
511+
512+
private getBaseSubscriptionClient() {
513+
if (!this.baseSubscriptionClient) {
514+
this.baseSubscriptionClient = this.newSubscriptionClient({ lazy: true });
515+
}
516+
return this.baseSubscriptionClient;
517+
}
506518
}
507519

508520
function processMaybeRelativeInput(input: RequestInfo | URL, endpoint: string): RequestInfo | URL {

0 commit comments

Comments
 (0)