Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great @evanshauser! I left quite a few comments, but most of them are minor. 🙂
Before we merge this in, we should also add at least one test with a mutation, which should be sent via POST. I'd also say that it should actually be configurable whether queries are sent over GET, but let's leave that for the future (maybe just leave a comment in the code about that). The reason for not wanting to send over GET sometimes is because many servers don't allow query strings to have more than 2000 characters or so.
@@ -32,16 +32,21 @@ | |||
"clean": "npm run clean:dist && npm run clean:coverage", | |||
"clean:dist": "rimraf dist/*", | |||
"clean:coverage": "rimraf coverage/*", | |||
"prepublish": "npm run clean && npm run build" | |||
"prepublishOnly": "npm run clean && npm run build" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this do? Is this a new npm thing?
src/httpFetcher.ts
Outdated
@@ -0,0 +1,175 @@ | |||
import { ApolloFetcher, Observable, FetchResult, Subscriber, Operation, State } from './types'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually break these over multiple lines if there's more than 2 or so.
src/httpFetcher.ts
Outdated
import { ApolloFetcher, Observable, FetchResult, Subscriber, Operation, State } from './types'; | ||
import { toSubscriber } from './subscriber'; | ||
import { print } from 'graphql'; | ||
import { DocumentNode, DefinitionNode, OperationDefinitionNode } from 'graphql/language/ast'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, multiple lines.
src/httpFetcher.ts
Outdated
|
||
public start() { | ||
if (this.state !== State.COLD) { | ||
throw Error('Observer already started'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if we should really throw here. It might be better to just do nothing.
src/httpFetcher.ts
Outdated
} | ||
|
||
public stop() { | ||
if (this.state === State.COMPLETED || this.state === State.ERRORED) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I'd just ignore it if we already stopped. That will make the operation idempotent which is easier to reason about.
src/subscriber.ts
Outdated
import { Subscriber } from './types'; | ||
|
||
function isSubscriber<T>(object: any): object is Subscriber<T> { | ||
return 'next' in object; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is next
always required for a subscriber? Or could it just have error
? Maybe the safer thing to check would be to see if it's a function
and if it is, treat it as the next
function.
src/types.ts
Outdated
} | ||
|
||
export interface Operation { | ||
query: DocumentNode; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is good for now, but I think eventually we'll also allow sending just the operation name (for persisted queries). Speaking of persisted queries, that's a use-case we should add to our list!!!
src/types.ts
Outdated
context?: object; | ||
} | ||
|
||
export type UnsubscribeHandler = () => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is now different in the TC39 proposal. Do we want to try to align here?
tests/index.ts
Outdated
observable.subscribe({ | ||
next, | ||
error: (error) => assert(false), | ||
complete: () => { assert(next.calledOnce); done(); }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tried if this test can fail at all?
tests/index.ts
Outdated
const next = sinon.spy(); | ||
const data = {hello: 'world'}; | ||
fetchMock.get('*', data); | ||
const fetcher = new HttpFetcher('', fetch); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should make the fetch
argument optional. We'll add a separate test for adding the argument later, which can use sinon.spy
to check that the function is getting called.
…Fetcher Interface
src/link.ts
Outdated
} | ||
|
||
private static buildLinkChain(links: ApolloLink[]): NextLink { | ||
const _links = [...links]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This way of creating the connections between the links makes a new array for each request. It mirrors the middleware and afterware code from the netowrk interface of apollo-client
(also in apollo-fetch
) the next
function on demand. We could create a closure and keep an index that we increment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@evanshauser I think we are missing some clarity around how forward works / what the user has to return. Lets chat more about this
super(); | ||
} | ||
|
||
public request(operation: Operation, forward: NextLink): Observable<FetchResult> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@evanshauser I'm concerned about the current forward
pattern since it could be optional? I'm a little confused at forward and how we even do things like enforcing passing a forward into a forward?
src/link.ts
Outdated
if (operation.variables === undefined) { | ||
operation.variables = {}; | ||
} | ||
if (operation.query === undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is this for?
src/link.ts
Outdated
} | ||
const _operation = transformOperation(operation); | ||
|
||
return link.request(_operation) || Observable.of(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should there be a forward
passed to this request?
…mpty args are terminating
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few general concerns:
- A good number of unused files - we can move them to some folder, or remove them entirely
- We aren't consistently using the utilities we define, like
toLink
. We should make sure to use them everywhere.
src/cacheLink.ts
Outdated
@@ -0,0 +1,44 @@ | |||
import { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is unused - can we move it into the tests, or an unused folder, or remove it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say lets remove it for now as to not confuse early adopters with a potential spec for caching
} | ||
|
||
public request(operation: Operation): Observable<FetchResult> | null { | ||
this.headers = operation.context && operation.context.headers || {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For every link, we should make sure to document what it expects on the context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, until we settle on a formal contextTypes
kind of API (and on improving the types), I think we could just note it above the class definition as inline docs?
src/httpUtils.ts
Outdated
@@ -0,0 +1,46 @@ | |||
import { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is not used. Let's remove it?
src/link.ts
Outdated
operation.variables = {}; | ||
} | ||
if (!operation.query) { | ||
console.warn(`query should either be a sting or GraphQL AST`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sting
-> string
src/link.ts
Outdated
operation.query = <DocumentNode>{}; | ||
} | ||
|
||
const _operation: Operation = transformOperation(operation); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe transformedOperation
? _operation
isn't a super descriptive name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets just move transformOperation
into the request call:
return link.request(transformOperation(operation)) || Observable.of();
@@ -0,0 +1,41 @@ | |||
import { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we move toLink
, isTerminating
, etc, here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes please!
src/mockLink.ts
Outdated
@@ -0,0 +1,22 @@ | |||
import { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not used
src/pollingLink.ts
Outdated
|
||
constructor(pollInterval?: number) { | ||
super(); | ||
this.pollInterval = pollInterval || 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say we should just throw if the poll interval is not set. 0
doesn't seem like a reasonable default.
src/pollingLink.ts
Outdated
|
||
const poll = (() => { | ||
forward(operation).subscribe(subscriber); | ||
}).bind(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the bind here is necessary.
src/retryLink.ts
Outdated
observer.complete(); | ||
}, | ||
// This causes an error, not sure why | ||
// complete: observer.complete |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably need observer.complete.bind(observer)
.
src/pollingLink.ts
Outdated
|
||
constructor(pollInterval?: number) { | ||
super(); | ||
this.pollInterval = pollInterval || 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be interval
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@evanshauser are you open to it being a function that given the operation, can determine the interval?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good!
if (!operation.context) { | ||
operation.context = {}; | ||
} | ||
operation.context = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a recursive merge, since a link could overwrite an entire object, such as headers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fwiw, recursive merges are really expensive :( I would say that this link overwrites and previous context of the same keys when being added.
@evanshauser when we call request within the ApolloLink
interface, thoughts on creating a copy of the operation and passing it down?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@evanshauser this is getting close! A few changes / question in here.
I would also really like to see some inline documentation above methods on the Link abstract. That way when we start iterating on it, we can make sure we stick / understand the original decisions!
Great stuff!
src/cacheLink.ts
Outdated
@@ -0,0 +1,44 @@ | |||
import { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say lets remove it for now as to not confuse early adopters with a potential spec for caching
} | ||
|
||
public request(operation: Operation): Observable<FetchResult> | null { | ||
this.headers = operation.context && operation.context.headers || {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, until we settle on a formal contextTypes
kind of API (and on improving the types), I think we could just note it above the class definition as inline docs?
return ApolloLink.empty(); | ||
} | ||
|
||
return links |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@evanshauser this is nitpicky (and totally not necessary to change), but with reduce
you can specify the starting point.
i.e.
links.map(toLink).reduce((x, y) => x.concat(y)), ApolloLink.empty())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbaxleyiii The issue I ran into was that reduce is applied, so that the initial argument is the first in the chain, so the function calls became:
ApolloLink.empty().concat(first).concat(second)...
We could do an ApolloLink.passthrough() as the initial value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah shoot, yep! thank you! Lets keep it as is
src/link.ts
Outdated
// join two Links together | ||
public concat(link: ApolloLink | RequestHandler): ApolloLink { | ||
if (this.request.length <= 1) { | ||
const warning = Object.assign( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we also use isTerminating(this)
to be consistent in checking?
src/link.ts
Outdated
} | ||
link = ApolloLink.toLink(link); | ||
|
||
return link.request.length <= 1 ? new TerminatedConcat(this, link) : new ConcatLink(this, link); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question here about using the method instead of rewriting the check
tests/httpLink.ts
Outdated
error: (error) => assert(false), | ||
complete: () => { | ||
const body = JSON.parse(fetchMock.lastCall()[1]['body']); | ||
assert.equal(body['query'], print(sampleMutation)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any reason for not using dot notation here?
describe('concat', () => { | ||
it('should concat a function', (done) => { | ||
const returnOne = new SetContextLink({ add: 1 }); | ||
const link = returnOne.concat((operation, forward) => Observable.of({ data: operation.context.add })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@evanshauser this shows my potential fear with forward :(
I'm assuming the library would not see this as a terminating link, yet forward is not used so it in essence is terminating. I think with copy paste / boilerplate, this could really get people confused
return forward(operation); | ||
}); | ||
const mock2 = new MockLink((op, forward) => Observable.of({ data: op.context.add + 2 })); | ||
const mock3 = new MockLink((op, forward) => Observable.of({ data: op.context.add + 3 })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lets move MockLink
to the /tests
folder so it won't be confused as an external link
tests/singleRequestLink.ts
Outdated
import SingleRequestLink from '../src/singleRequestLink'; | ||
|
||
|
||
describe('SingleRequestLink', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is needed since the link won't be shipped. If it is, this needs a lot more tests
@@ -0,0 +1,7 @@ | |||
import './httpLink'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this file needed? Can you not glob test?
…ved SingleRequestLink
return ApolloLink.empty(); | ||
} | ||
|
||
return links |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah shoot, yep! thank you! Lets keep it as is
This includes the most basic fetcher, http-fetcher with a couple of tests. There are a couple constructs that illustrate possible additions to the specification and a bunch of notes for additions that need to be made to make this http-fetcher a canonical example.