Repository: FormidableLabs/urql
Pull request #1638 (2021-05-12 12:18:43)

(core) - Refactor Client to hide some impl details and reduce size
- Create an `interface Client` type with all internals hidden
- Convert `Client` to an ES5 factory/constructor pattern
- Ensure that `Client` can both be called with or without `new` (types remain the same)
- Hide internals in factory function using scoped closure variables as needed

`3.68kB minzipped` to `3.59kB minzipped`


Patch:
diff --git a/.changeset/plenty-frogs-remember.md b/.changeset/plenty-frogs-remember.md
new file mode 100644
index 00000000..e6b2dc30
--- /dev/null
+++ b/.changeset/plenty-frogs-remember.md
@@ -0,0 +1,5 @@
+---
+'@urql/core': patch
+---
+
+Refactor `Client` to hide some implementation details and to reduce size.
diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts
index 67f11cb4..892c2535 100755
--- a/packages/core/src/client.ts
+++ b/packages/core/src/client.ts
@@ -68,19 +68,14 @@ export interface ClientOptions {
   maskTypename?: boolean;
 }
 
-export const createClient = (opts: ClientOptions) => new Client(opts);
+export interface Client {
+  new (options: ClientOptions): Client;
 
-interface ActiveOperation {
-  source: Source<OperationResult>;
-  replay: OperationResult | null;
-}
+  operations$: Source<Operation>;
 
-/** The URQL application-wide client library. Each execute method starts a GraphQL request and returns a stream of results. */
-export class Client {
   /** Start an operation from an exchange */
   reexecuteOperation: (operation: Operation) => void;
-
-  // Event target for monitoring
+  /** Event target for monitoring, e.g. for @urql/devtools */
   subscribeToDebugTarget?: (onEvent: (e: DebugEvent) => void) => Subscription;
 
   // These are variables derived from ClientOptions
@@ -88,316 +83,344 @@ export class Client {
   fetch?: typeof fetch;
   fetchOptions?: RequestInit | (() => RequestInit);
   suspense: boolean;
-  preferGetMethod: boolean;
   requestPolicy: RequestPolicy;
+  preferGetMethod: boolean;
   maskTypename: boolean;
 
-  // These are internals to be used to keep track of operations
-  dispatchOperation: (operation?: Operation | void) => void;
-  operations$: Source<Operation>;
-  results$: Source<OperationResult>;
-  activeOperations: Map<number, ActiveOperation> = new Map();
-  queue: Operation[] = [];
-
-  constructor(opts: ClientOptions) {
-    if (process.env.NODE_ENV !== 'production' && !opts.url) {
-      throw new Error('You are creating an urql-client without a url.');
-    }
-
-    let dispatchDebug: ExchangeInput['dispatchDebug'] = noop;
-    if (process.env.NODE_ENV !== 'production') {
-      const { next, source } = makeSubject<DebugEvent>();
-      this.subscribeToDebugTarget = (onEvent: (e: DebugEvent) => void) =>
-        pipe(source, subscribe(onEvent));
-      dispatchDebug = next as ExchangeInput['dispatchDebug'];
-    }
-
-    this.url = opts.url;
-    this.fetchOptions = opts.fetchOptions;
-    this.fetch = opts.fetch;
-    this.suspense = !!opts.suspense;
-    this.requestPolicy = opts.requestPolicy || 'cache-first';
-    this.preferGetMethod = !!opts.preferGetMethod;
-    this.maskTypename = !!opts.maskTypename;
-
-    // This subject forms the input of operations; executeOperation may be
-    // called to dispatch a new operation on the subject
-    const {
-      source: operations$,
-      next: nextOperation,
-    } = makeSubject<Operation>();
-    this.operations$ = operations$;
-
-    let isOperationBatchActive = false;
-    this.dispatchOperation = (operation?: Operation | void) => {
-      isOperationBatchActive = true;
-      if (operation) nextOperation(operation);
-      while ((operation = this.queue.shift())) nextOperation(operation);
-      isOperationBatchActive = false;
-    };
-
-    this.reexecuteOperation = (operation: Operation) => {
-      // Reexecute operation only if any subscribers are still subscribed to the
-      // operation's exchange results
-      if (
-        operation.kind === 'mutation' ||
-        this.activeOperations.has(operation.key)
-      ) {
-        this.queue.push(operation);
-        if (!isOperationBatchActive) {
-          Promise.resolve().then(this.dispatchOperation);
-        }
-      }
-    };
-
-    const exchanges =
-      opts.exchanges !== undefined ? opts.exchanges : defaultExchanges;
-
-    // All exchange are composed into a single one and are called using the constructed client
-    // and the fallback exchange stream
-    const composedExchange = composeExchanges(exchanges);
-
-    // All exchanges receive inputs using which they can forward operations to the next exchange
-    // and receive a stream of results in return, access the client, or dispatch debugging events
-    // All operations then run through the Exchange IOs in a pipeline-like fashion
-    this.results$ = share(
-      composedExchange({
-        client: this,
-        dispatchDebug,
-        forward: fallbackExchange({ dispatchDebug }),
-      })(this.operations$)
-    );
+  createOperationContext(
+    opts?: Partial<OperationContext> | undefined
+  ): OperationContext;
 
-    // Prevent the `results$` exchange pipeline from being closed by active
-    // cancellations cascading up from components
-    pipe(this.results$, publish);
-  }
-
-  createOperationContext = (
-    opts?: Partial<OperationContext>
-  ): OperationContext => {
-    if (!opts) opts = {};
-
-    return {
-      url: this.url,
-      fetchOptions: this.fetchOptions,
-      fetch: this.fetch,
-      preferGetMethod: this.preferGetMethod,
-      ...opts,
-      suspense: opts.suspense || (opts.suspense !== false && this.suspense),
-      requestPolicy: opts.requestPolicy || this.requestPolicy,
-    };
-  };
-
-  createRequestOperation = <Data = any, Variables = object>(
+  createRequestOperation<Data = any, Variables = object>(
     kind: OperationType,
     request: GraphQLRequest<Data, Variables>,
-    opts?: Partial<OperationContext>
-  ): Operation<Data, Variables> =>
-    makeOperation<Data, Variables>(
-      kind,
-      request,
-      this.createOperationContext(opts)
-    );
+    opts?: Partial<OperationContext> | undefined
+  ): Operation<Data, Variables>;
 
   /** Executes an Operation by sending it through the exchange pipeline It returns an observable that emits all related exchange results and keeps track of this observable's subscribers. A teardown signal will be emitted when no subscribers are listening anymore. */
   executeRequestOperation<Data = any, Variables = object>(
     operation: Operation<Data, Variables>
-  ): Source<OperationResult<Data, Variables>> {
-    let activeOperation = this.activeOperations.get(operation.key);
-    if (!activeOperation) {
-      let result$ = pipe(
-        this.results$,
-        filter(
-          (res: OperationResult) =>
-            res.operation.kind === operation.kind &&
-            res.operation.key === operation.key
-        )
-      );
-
-      // Mask typename properties if the option for it is turned on
-      if (this.maskTypename) {
-        result$ = pipe(
-          result$,
-          map(res => ({ ...res, data: maskTypename(res.data) }))
-        );
-      }
-
-      // A mutation is always limited to just a single result and is never shared
-      if (operation.kind === 'mutation') {
-        return pipe(
-          result$,
-          onStart(() => this.dispatchOperation(operation)),
-          take(1)
-        );
-      }
-
-      activeOperation = {
-        source: pipe(
-          result$,
-          // End the results stream when an active teardown event is sent
-          takeUntil(
-            pipe(
-              this.operations$,
-              filter(op => op.kind === 'teardown' && op.key === operation.key)
-            )
-          ),
-          switchMap(result => {
-            if (result.stale) {
-              return fromValue(result);
-            }
-
-            return merge([
-              fromValue(result),
-              // Mark a result as stale when a new operation is sent for it
-              pipe(
-                this.operations$,
-                filter(op => {
-                  return (
-                    op.kind === operation.kind &&
-                    op.key === operation.key &&
-                    (op.context.requestPolicy === 'network-only' ||
-                      op.context.requestPolicy === 'cache-and-network')
-                  );
-                }),
-                take(1),
-                map(() => ({ ...result, stale: true }))
-              ),
-            ]);
-          }),
-          onPush(result => {
-            activeOperation!.replay = result;
-          }),
-          onEnd(() => {
-            // Delete the active operation handle
-            activeOperation!.replay = null;
-            this.activeOperations.delete(operation.key);
-            // Delete all queued up operations of the same key on end
-            for (let i = this.queue.length - 1; i >= 0; i--)
-              if (this.queue[i].key === operation.key) this.queue.splice(i, 1);
-            // Dispatch a teardown signal for the stopped operation
-            this.dispatchOperation(
-              makeOperation('teardown', operation, operation.context)
-            );
-          }),
-          share
-        ),
-        replay: null,
-      };
-    }
-
-    const isNetworkOperation =
-      operation.context.requestPolicy === 'cache-and-network' ||
-      operation.context.requestPolicy === 'network-only';
-
-    return make(observer => {
-      activeOperation =
-        this.activeOperations.get(operation.key) || activeOperation!;
-      const prevReplay = activeOperation.replay;
-
-      return pipe(
-        activeOperation.source,
-        onStart(() => {
-          this.activeOperations.set(operation.key, activeOperation!);
-          if (operation.kind === 'subscription') {
-            return this.dispatchOperation(operation);
-          } else if (isNetworkOperation) {
-            this.dispatchOperation(operation);
-          }
-
-          if (prevReplay != null && prevReplay === activeOperation!.replay) {
-            observer.next(
-              isNetworkOperation ? { ...prevReplay, stale: true } : prevReplay
-            );
-          } else if (!isNetworkOperation) {
-            this.dispatchOperation(operation);
-          }
-        }),
-        onEnd(observer.complete),
-        subscribe(observer.next)
-      ).unsubscribe;
-    });
-  }
+  ): Source<OperationResult<Data, Variables>>;
 
   query<Data = any, Variables extends object = {}>(
     query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
     variables?: Variables,
     context?: Partial<OperationContext>
-  ): PromisifiedSource<OperationResult<Data, Variables>> {
-    if (!context || typeof context.suspense !== 'boolean') {
-      context = { ...context, suspense: false };
-    }
-
-    return withPromise<OperationResult<Data, Variables>>(
-      this.executeQuery<Data, Variables>(
-        createRequest(query, variables),
-        context
-      )
-    );
-  }
+  ): PromisifiedSource<OperationResult<Data, Variables>>;
 
   readQuery<Data = any, Variables extends object = {}>(
     query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
     variables?: Variables,
     context?: Partial<OperationContext>
-  ): OperationResult<Data, Variables> | null {
-    let result: OperationResult<Data, Variables> | null = null;
+  ): OperationResult<Data, Variables> | null;
 
-    pipe(
-      this.query(query, variables, context),
-      subscribe(res => {
-        result = res;
-      })
-    ).unsubscribe();
-
-    return result;
-  }
-
-  executeQuery = <Data = any, Variables = object>(
+  executeQuery<Data = any, Variables = object>(
     query: GraphQLRequest<Data, Variables>,
-    opts?: Partial<OperationContext>
-  ): Source<OperationResult<Data, Variables>> => {
-    const operation = this.createRequestOperation('query', query, opts);
-    return this.executeRequestOperation<Data, Variables>(operation);
-  };
+    opts?: Partial<OperationContext> | undefined
+  ): Source<OperationResult<Data, Variables>>;
 
   subscription<Data = any, Variables extends object = {}>(
     query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
     variables?: Variables,
     context?: Partial<OperationContext>
-  ): Source<OperationResult<Data, Variables>> {
-    return this.executeSubscription<Data, Variables>(
-      createRequest(query, variables),
-      context
-    );
-  }
+  ): Source<OperationResult<Data, Variables>>;
 
-  executeSubscription = <Data = any, Variables = object>(
+  executeSubscription<Data = any, Variables = object>(
     query: GraphQLRequest<Data, Variables>,
-    opts?: Partial<OperationContext>
-  ): Source<OperationResult<Data, Variables>> => {
-    const operation = this.createRequestOperation('subscription', query, opts);
-    return this.executeRequestOperation<Data, Variables>(operation);
-  };
+    opts?: Partial<OperationContext> | undefined
+  ): Source<OperationResult<Data, Variables>>;
 
   mutation<Data = any, Variables extends object = {}>(
     query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
     variables?: Variables,
     context?: Partial<OperationContext>
-  ): PromisifiedSource<OperationResult<Data, Variables>> {
-    return withPromise<OperationResult<Data, Variables>>(
-      this.executeMutation<Data, Variables>(
-        createRequest(query, variables),
-        context
+  ): PromisifiedSource<OperationResult<Data, Variables>>;
+
+  executeMutation<Data = any, Variables = object>(
+    query: GraphQLRequest<Data, Variables>,
+    opts?: Partial<OperationContext> | undefined
+  ): Source<OperationResult<Data, Variables>>;
+}
+
+export const Client: new (opts: ClientOptions) => Client = function Client(
+  this: Client | {},
+  opts: ClientOptions
+) {
+  if (process.env.NODE_ENV !== 'production' && !opts.url) {
+    throw new Error('You are creating an urql-client without a url.');
+  }
+
+  const replays = new Map<number, OperationResult>();
+  const active: Map<number, Source<OperationResult>> = new Map();
+  const queue: Operation[] = [];
+
+  // This subject forms the input of operations; executeOperation may be
+  // called to dispatch a new operation on the subject
+  const { source: operations$, next: nextOperation } = makeSubject<Operation>();
+
+  // We define a queued dispatcher on the subject, which empties the queue when it's
+  // activated to allow `reexecuteOperation` to be trampoline-scheduled
+  let isOperationBatchActive = false;
+  function dispatchOperation(operation?: Operation | void) {
+    isOperationBatchActive = true;
+    if (operation) nextOperation(operation);
+    while ((operation = queue.shift())) nextOperation(operation);
+    isOperationBatchActive = false;
+  }
+
+  /** Defines how result streams are created */
+  const makeResultSource = (operation: Operation) => {
+    let result$ = pipe(
+      results$,
+      filter(
+        (res: OperationResult) =>
+          res.operation.kind === operation.kind &&
+          res.operation.key === operation.key
       )
     );
-  }
 
-  executeMutation = <Data = any, Variables = object>(
-    query: GraphQLRequest<Data, Variables>,
-    opts?: Partial<OperationContext>
-  ): Source<OperationResult<Data, Variables>> => {
-    const operation = this.createRequestOperation('mutation', query, opts);
-    return this.executeRequestOperation<Data, Variables>(operation);
+    // Mask typename properties if the option for it is turned on
+    if (client.maskTypename) {
+      result$ = pipe(
+        result$,
+        map(res => ({ ...res, data: maskTypename(res.data) }))
+      );
+    }
+
+    // A mutation is always limited to just a single result and is never shared
+    if (operation.kind === 'mutation') {
+      return pipe(
+        result$,
+        onStart(() => dispatchOperation(operation)),
+        take(1)
+      );
+    }
+
+    const source = pipe(
+      result$,
+      // End the results stream when an active teardown event is sent
+      takeUntil(
+        pipe(
+          operations$,
+          filter(op => op.kind === 'teardown' && op.key === operation.key)
+        )
+      ),
+      switchMap(result => {
+        if (result.stale) {
+          return fromValue(result);
+        }
+
+        return merge([
+          fromValue(result),
+          // Mark a result as stale when a new operation is sent for it
+          pipe(
+            operations$,
+            filter(op => {
+              return (
+                op.kind === operation.kind &&
+                op.key === operation.key &&
+                (op.context.requestPolicy === 'network-only' ||
+                  op.context.requestPolicy === 'cache-and-network')
+              );
+            }),
+            take(1),
+            map(() => ({ ...result, stale: true }))
+          ),
+        ]);
+      }),
+      onPush(result => {
+        replays.set(operation.key, result);
+      }),
+      onStart(() => {
+        active.set(operation.key, source);
+      }),
+      onEnd(() => {
+        // Delete the active operation handle
+        replays.delete(operation.key);
+        active.delete(operation.key);
+        // Delete all queued up operations of the same key on end
+        for (let i = queue.length - 1; i >= 0; i--)
+          if (queue[i].key === operation.key) queue.splice(i, 1);
+        // Dispatch a teardown signal for the stopped operation
+        dispatchOperation(
+          makeOperation('teardown', operation, operation.context)
+        );
+      }),
+      share
+    );
+
+    return source;
   };
-}
+
+  const instance: Client =
+    this instanceof Client ? this : Object.create(Client.prototype);
+  const client: Client = Object.assign(instance, {
+    url: opts.url,
+    fetchOptions: opts.fetchOptions,
+    fetch: opts.fetch,
+    suspense: !!opts.suspense,
+    requestPolicy: opts.requestPolicy || 'cache-first',
+    preferGetMethod: !!opts.preferGetMethod,
+    maskTypename: !!opts.maskTypename,
+
+    operations$,
+
+    reexecuteOperation(operation: Operation) {
+      // Reexecute operation only if any subscribers are still subscribed to the
+      // operation's exchange results
+      if (operation.kind === 'mutation' || active.has(operation.key)) {
+        queue.push(operation);
+        if (!isOperationBatchActive) {
+          Promise.resolve().then(dispatchOperation);
+        }
+      }
+    },
+
+    createOperationContext(opts) {
+      if (!opts) opts = {};
+
+      return {
+        url: client.url,
+        fetchOptions: client.fetchOptions,
+        fetch: client.fetch,
+        preferGetMethod: client.preferGetMethod,
+        ...opts,
+        suspense: opts.suspense || (opts.suspense !== false && client.suspense),
+        requestPolicy: opts.requestPolicy || client.requestPolicy,
+      };
+    },
+
+    createRequestOperation(kind, request, opts) {
+      return makeOperation(kind, request, client.createOperationContext(opts));
+    },
+
+    executeRequestOperation(operation) {
+      if (operation.kind === 'mutation') {
+        return makeResultSource(operation);
+      }
+
+      const source = active.get(operation.key) || makeResultSource(operation);
+
+      const isNetworkOperation =
+        operation.context.requestPolicy === 'cache-and-network' ||
+        operation.context.requestPolicy === 'network-only';
+
+      return make(observer => {
+        return pipe(
+          source,
+          onStart(() => {
+            const prevReplay = replays.get(operation.key);
+
+            if (operation.kind === 'subscription') {
+              return dispatchOperation(operation);
+            } else if (isNetworkOperation) {
+              dispatchOperation(operation);
+            }
+
+            if (
+              prevReplay != null &&
+              prevReplay === replays.get(operation.key)
+            ) {
+              observer.next(
+                isNetworkOperation ? { ...prevReplay, stale: true } : prevReplay
+              );
+            } else if (!isNetworkOperation) {
+              dispatchOperation(operation);
+            }
+          }),
+          onEnd(observer.complete),
+          subscribe(observer.next)
+        ).unsubscribe;
+      });
+    },
+
+    executeQuery(query, opts) {
+      const operation = client.createRequestOperation('query', query, opts);
+      return client.executeRequestOperation(operation);
+    },
+
+    executeSubscription(query, opts) {
+      const operation = client.createRequestOperation(
+        'subscription',
+        query,
+        opts
+      );
+      return client.executeRequestOperation(operation);
+    },
+
+    executeMutation(query, opts) {
+      const operation = client.createRequestOperation('mutation', query, opts);
+      return client.executeRequestOperation(operation);
+    },
+
+    query(query, variables, context) {
+      if (!context || typeof context.suspense !== 'boolean') {
+        context = { ...context, suspense: false };
+      }
+
+      return withPromise(
+        client.executeQuery(createRequest(query, variables), context)
+      );
+    },
+
+    readQuery(query, variables, context) {
+      let result: OperationResult | null = null;
+
+      pipe(
+        client.query(query, variables, context),
+        subscribe(res => {
+          result = res;
+        })
+      ).unsubscribe();
+
+      return result;
+    },
+
+    subscription(query, variables, context) {
+      return client.executeSubscription(
+        createRequest(query, variables),
+        context
+      );
+    },
+
+    mutation(query, variables, context) {
+      return withPromise(
+        client.executeMutation(createRequest(query, variables), context)
+      );
+    },
+  } as Client);
+
+  let dispatchDebug: ExchangeInput['dispatchDebug'] = noop;
+  if (process.env.NODE_ENV !== 'production') {
+    const { next, source } = makeSubject<DebugEvent>();
+    client.subscribeToDebugTarget = (onEvent: (e: DebugEvent) => void) =>
+      pipe(source, subscribe(onEvent));
+    dispatchDebug = next as ExchangeInput['dispatchDebug'];
+  }
+
+  const exchanges =
+    opts.exchanges !== undefined ? opts.exchanges : defaultExchanges;
+
+  // All exchange are composed into a single one and are called using the constructed client
+  // and the fallback exchange stream
+  const composedExchange = composeExchanges(exchanges);
+
+  // All exchanges receive inputs using which they can forward operations to the next exchange
+  // and receive a stream of results in return, access the client, or dispatch debugging events
+  // All operations then run through the Exchange IOs in a pipeline-like fashion
+  const results$ = share(
+    composedExchange({
+      client,
+      dispatchDebug,
+      forward: fallbackExchange({ dispatchDebug }),
+    })(operations$)
+  );
+
+  // Prevent the `results$` exchange pipeline from being closed by active
+  // cancellations cascading up from components
+  pipe(results$, publish);
+
+  return client;
+} as any;
+
+export const createClient = (Client as any) as (opts: ClientOptions) => Client;
diff --git a/scripts/eslint/preset.js b/scripts/eslint/preset.js
index 15c4690e..0e2d9970 100644
--- a/scripts/eslint/preset.js
+++ b/scripts/eslint/preset.js
@@ -16,6 +16,7 @@ module.exports = {
     '@typescript-eslint/explicit-function-return-type': 'off',
     '@typescript-eslint/interface-name-prefix': 'off',
     '@typescript-eslint/no-non-null-assertion': 'off',
+    '@typescript-eslint/no-misused-new': 'off',
     '@typescript-eslint/no-explicit-any': 'off',
     '@typescript-eslint/array-type': 'off',
     'import/no-internal-modules': 'off',


Test patch:
diff --git a/packages/core/src/__snapshots__/client.test.ts.snap b/packages/core/src/__snapshots__/client.test.ts.snap
index d49c1ab2..2bf733fd 100644
--- a/packages/core/src/__snapshots__/client.test.ts.snap
+++ b/packages/core/src/__snapshots__/client.test.ts.snap
@@ -1,24 +1,25 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`createClient passes snapshot 1`] = `
+exports[`createClient / Client passes snapshot 1`] = `
 Client {
-  "activeOperations": Map {},
   "createOperationContext": [Function],
   "createRequestOperation": [Function],
-  "dispatchOperation": [Function],
   "executeMutation": [Function],
   "executeQuery": [Function],
+  "executeRequestOperation": [Function],
   "executeSubscription": [Function],
   "fetch": undefined,
   "fetchOptions": undefined,
   "maskTypename": false,
+  "mutation": [Function],
   "operations$": [Function],
   "preferGetMethod": false,
-  "queue": Array [],
+  "query": [Function],
+  "readQuery": [Function],
   "reexecuteOperation": [Function],
   "requestPolicy": "cache-first",
-  "results$": [Function],
   "subscribeToDebugTarget": [Function],
+  "subscription": [Function],
   "suspense": false,
   "url": "https://hostname.com",
 }
diff --git a/packages/core/src/client.test.ts b/packages/core/src/client.test.ts
index 5ee56c2f..0e4e84a8 100755
--- a/packages/core/src/client.test.ts
+++ b/packages/core/src/client.test.ts
@@ -8,9 +8,12 @@ import {
   map,
   pipe,
   subscribe,
+  publish,
   filter,
+  share,
   toArray,
   toPromise,
+  onPush,
   tap,
   take,
 } from 'wonka';
@@ -18,18 +21,20 @@ import {
 import { gql } from './gql';
 import { Exchange, Operation, OperationResult } from './types';
 import { makeOperation } from './utils';
-import { createClient } from './client';
+import { Client, createClient } from './client';
 import { queryOperation, subscriptionOperation } from './test-utils';
 
 const url = 'https://hostname.com';
 
-describe('createClient', () => {
-  it('passes snapshot', () => {
-    const c = createClient({
-      url,
-    });
+describe('createClient / Client', () => {
+  it('creates an instance of Client', () => {
+    expect(createClient({ url }) instanceof Client).toBeTruthy();
+    expect(new Client({ url }) instanceof Client).toBeTruthy();
+  });
 
-    expect(c).toMatchSnapshot();
+  it('passes snapshot', () => {
+    const client = createClient({ url });
+    expect(client).toMatchSnapshot();
   });
 });
 
@@ -344,19 +349,16 @@ describe('queuing behavior', () => {
       exchanges: [exchange],
     });
 
-    pipe(
-      client.results$,
-      subscribe(result => {
-        output.push(result);
-      })
-    );
-
-    const results = pipe(
+    const shared = pipe(
       client.executeRequestOperation(queryOperation),
-      toArray
+      onPush(result => output.push(result)),
+      share
     );
 
-    expect(output.length).toBe(4);
+    const results = pipe(shared, toArray);
+    pipe(shared, publish);
+
+    expect(output.length).toBe(8);
     expect(results.length).toBe(2);
 
     expect(output[0]).toHaveProperty('key', queryOperation.key);
diff --git a/packages/next-urql/src/__tests__/with-urql-client.spec.ts b/packages/next-urql/src/__tests__/with-urql-client.spec.ts
index 6eb0151b..d8ce4aae 100644
--- a/packages/next-urql/src/__tests__/with-urql-client.spec.ts
+++ b/packages/next-urql/src/__tests__/with-urql-client.spec.ts
@@ -43,7 +43,6 @@ describe('withUrqlClient', () => {
       const tree = shallow(h(Component));
       const app = tree.find(MockApp);
 
-      expect(app.props().urqlClient).toBeInstanceOf(Client);
       expect(app.props().urqlClient.url).toBe('http://localhost:3000');
       expect(spyInitUrqlClient).toHaveBeenCalledTimes(1);
       expect(spyInitUrqlClient.mock.calls[0][0].exchanges).toHaveLength(4);
@@ -58,7 +57,6 @@ describe('withUrqlClient', () => {
       const tree = shallow(h(Component, props));
       const app = tree.find(MockApp);
 
-      expect(app.props().urqlClient).toBeInstanceOf(Client);
       expect(app.props().urqlClient.url).toEqual('http://localhost:3000');
     });
   });