Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple layer #3

Merged
merged 13 commits into from
Jan 15, 2020
Merged

Simple layer #3

merged 13 commits into from
Jan 15, 2020

Conversation

jquense
Copy link
Member

@jquense jquense commented Jan 7, 2020

rips out RRMNL for something much smaller. I should maybe keep the socket stuff split out to avoid including socketio unnecessarily

@jquense jquense requested review from taion and AaronBuxbaum January 7, 2020 21:37
@itajaja
Copy link
Member

itajaja commented Jan 7, 2020

rips out RRMNL for something much smaller.

what's the cost/benefit of having this logic in here vs using an upstream package, if they are functionally equivalent?

@jquense
Copy link
Member Author

jquense commented Jan 8, 2020

The upstream package includes a lot more code/logic over what is essentially a fetch helper in an attempt to cover every possible use case. I think there is space for a something like this that is way more minimal, but covers the 80% case.

RRMNL also is badly packaged/published. We are pinned to an old version b/c the author is using the babel runtime transform, but not including babel runtime or corejs as dependencies because "it bloats the bundle". i.e. is broken by default but it's being called working as intended so IDK

@jquense
Copy link
Member Author

jquense commented Jan 8, 2020

also this is now better tested than the upstream one :P

@jquense
Copy link
Member Author

jquense commented Jan 8, 2020

I think the API for a general package we could stick in relay-tools should probably be:

import { NetworkLayer } from 'relay-runtime'
import { createFetch, createSubscribe } from 'relay-network-layer'

NetworkLayer.create(
  createFetch(opts), 
  createSubscribe(otherOpts)
)

@itajaja
Copy link
Member

itajaja commented Jan 8, 2020

thanks :) and having this as a separate network layer package? does it make sense?

@jquense
Copy link
Member Author

jquense commented Jan 8, 2020

I was thinking of renaming this to 'relay-network-layer' and having the useAuthToken bit just be a side feature. WDT?

@itajaja
Copy link
Member

itajaja commented Jan 8, 2020

might make more sense. I don't have big vesting on this so my suggestions are totally non binding :) I'll let you guys decide!

@taion
Copy link
Contributor

taion commented Jan 8, 2020

did you not like my relay-fetch as a library name? https://github.com/4Catalyzer/relay-fetch

@jquense
Copy link
Member Author

jquense commented Jan 9, 2020

It's also good :P i already had the work here. we can put it where ever. I mostly like that relay-network-layer is available on npm and feels more canonical than RMNL, in a petty sort of way.

@taion
Copy link
Contributor

taion commented Jan 9, 2020

okay, relay-network-layer does in fact sound better

@jquense
Copy link
Member Author

jquense commented Jan 10, 2020

gtg?

Copy link
Contributor

@taion taion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this might be simpler without observables. For basic batching, you could do something like:

function fetchFn(request) {
  if (!canBatch(request)) {
    return fetchGraphql(request);
  }

  return batcher.fetch(request);
}

class Batcher {
  pendingRequests: Array<PendingRequest> | null = null;
  
  fetch(request) {
    if (!this.pendingRequests) {
      this.pendingRequests = [];
      
      setTimeout(this.executeBatch, timeoutMs);
    }
    
    return new Promise((resolve, reject) => {
      this.pendingRequests.push({
        request,
        resolve,
        reject,
      });
    });
  }
  
  executeBatch = async () => {
    const results = await fetchData(
      this.pendingRequests.map(({ request }) => request),
    );
    
    results.forEach((result, i) => {
      if (result.errors) {
        this.pendingRequests[i].reject(...);
      } else {
        this.pendingRequests[i].resolve(...);
      }
    });
  }
}

this is obviously very sketch-quality, but it seems like we can generally avoid observables here?

});
});

export default (origin, opts) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this just a very small reimpl of an event emitter?

it works, i'm sure, but... could just use an event emitter :p

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i know, i just copied and tweaked from some medium post on mocking socket.io

batch?: boolean;

/** An authorization token sent in a header with every request */
token?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
token?: string;
authorizationToken?: string;

maybe? more explicit name

alternatively, maybe something like:

authorization?: string | {
  headerName?: string;
  scheme?: string;
  token: string;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this can be removed

socket
.on('connect', () => {
if (token) {
emitTransient('authenticate', token);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, huh, so we don't ever update auth?

i wonder if we can move to something like the apollo spec where we just send auth at connect time, then.

it's a little awkward, though, because i think in a few places we may subscribe before we auth? maybe just for telemed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually wondering if there is a (minor) bug around here. We tear down network layers before the environment has been replaced. Envs don't really have any concept of a closed network so presumably until the environment is not used any more it'd still be trying to subscribe...

Also tho, is it actually feasible to update auth for existing subscriptions? If you switch to another user without permission would existing sub's just fail indefinitely? Obviously this doesn't really ever occur since we teardown everything on user changes. But still seems kinda sharp, really you'd only want to update auth in a refresh token sort of situation which we don't currently have.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, this was some holdover from the old q-si stuff where you could refresh your token expiration. that hasn't been possible for quite some time now, so maybe this use case has gone away

i mean, emitTransient does nothing anyway if the subscription is closed, right? so it's not a big deal

* Any fetch API "init" details, this is based directly to the `fetch` call, see
* [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) for API details.
*/
init?: RequestInit;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this API is somewhat awkward because, as constructed, it doesn't let us add extra headers or whatever

perhaps just something that accepts a custom fn with the same signature as fetch, to allow maximum flexibility?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why doesn't it allow extra headers? if you add headers in here they are used as the defaults.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, didn't see that you initialized headers with init?.headers

still... could be more flexible to accept a custom fetch fn that just defaults to window.fetch? or do you think it's overkill?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, this is probably enough for now

*
* **Requires a Graphql server that understands batching"
*/
batch?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe:

batching?: boolean | {
  enabled: boolean;
  timeoutMs?: number;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meh, makes it a bit harder to handle, I'd so not worth it for one additional option

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternatively, could jsut collapse this into one option that is batch timeout: false | number

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, that works too. i got this pattern from apollo server config, where it seems... maybe fairly neat from the user pov?

@jquense
Copy link
Member Author

jquense commented Jan 13, 2020

I feel like this might be simpler without observables.

I was thinking the observables were required but I guess if we are always using the builtin Network.create it'll coerce the Promise to an observable. The real reason for the observable is the abort logic tho.

Co-Authored-By: Jimmy Jia <[email protected]>
@taion
Copy link
Contributor

taion commented Jan 13, 2020

Why do you need the observable for the abort logic? Can't you just reject the promise on any sort of error?

@jquense
Copy link
Member Author

jquense commented Jan 13, 2020

idk how Relay converts it, the result from an abort is you complete the observable without having sent a value, with a Promise, that's a bit ambiguous

@taion
Copy link
Contributor

taion commented Jan 13, 2020

okay, that makes sense. that's definitely worth adding a comment to note

@jquense jquense requested a review from taion January 14, 2020 16:32
@jquense
Copy link
Member Author

jquense commented Jan 14, 2020

notes addressed!

Copy link
Contributor

@taion taion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, nothing really big... just the notes here

README.md Outdated
};

/** An authorization token sent in a header with every request */
token?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so remove this option and just use authorization below?

and in the example above

batch?: boolean;

/** An authorization token sent in a header with every request */
token?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this can be removed

* Any fetch API "init" details, this is based directly to the `fetch` call, see
* [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) for API details.
*/
init?: RequestInit;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, this is probably enough for now


constructor({
const SimpleNetworkLayer = {
create({
token,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
token,

and just use authorization below

throwErrors,
init,
batch,
url = '/graphql',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sort these options to match their order in the docs above?

init?: RequestInit;
throwErrors?: boolean;
authorization?:
| string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this accept some sort of boolean or null for an explicit false?


export interface SubscriptionClientOptions {
url?: string;
token?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps this can take the same shape with authorization?: ... as in the non-subscription version?

new (...args: any): SubscriptionClient;
}

export class SocketioSubscriptionClient implements SubscriptionClient {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export class SocketioSubscriptionClient implements SubscriptionClient {
export class SocketIoSubscriptionClient implements SubscriptionClient {

close?(): void | Promise<void>;
}

interface SubscriptionClientClass {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could do Class<SubscriptionClient> with Class from utility-types

}

export default function createSubscribe({
subscriptionClientClass = SocketioSubscriptionClient,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you know if it's more standard for this to be called subscriptionClientClass or should it be SubscriptionClient?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im not aware of any consensus on that

@jquense jquense merged commit fc3408d into master Jan 15, 2020
@jquense jquense deleted the simple-layer branch January 15, 2020 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants