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

subscribeToMore for observableQuery #797

Merged
merged 7 commits into from
Oct 18, 2016
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Expect active development and potentially significant breaking changes in the `0
- Fix multidimentional array handling. [Issue #776](https://github.com/apollostack/apollo-client/issues/776) [PR #785](https://github.com/apollostack/apollo-client/pull/785)
- Add support for Enum inline arguments [Issue #183](https://github.com/apollostack/apollo-client/issues/183) [PR #788](https://github.com/apollostack/apollo-client/pull/788)
- Make it possible to subscribe to the same observable query multiple times. The query is initialized on the first subscription, and torn down after the last. Now, QueryManager is only aware of one subscription from the ObservableQuery, no matter how many were actually registered. This fixes issues with `result()` and other ObservableQuery features that relied on subscribing multiple times to work. This should remove the need for the workaround in `0.4.21`. [Repro in PR #694](https://github.com/apollostack/apollo-client/pull/694) [PR #791](https://github.com/apollostack/apollo-client/pull/791)
- ** new Feature **: Add fetchMore-style subscribeToMore function which updates a query result based on a subscription. [PR #797](https://github.com/apollostack/apollo-client/pull/797)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think you're allowed to put spaces between the ** like this in markdown.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that's funny, because I think I copy-pasted it from another entry :D Will fix both.


### v0.4.21
- Added some temporary functions (`_setVariablesNoResult` and `_setOptionsNoResult`) to work around a `react-apollo` problem fundamentally caused by the issue highlighted in [PR #694](https://github.com/apollostack/apollo-client/pull/694). The code as been refactored on `master`, so we expect it to be fixed in 0.5.x, and is not worth resolving now.
Expand Down
54 changes: 52 additions & 2 deletions src/core/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import {
ModifiableWatchQueryOptions,
WatchQueryOptions,
FetchMoreQueryOptions,
SubscribeToMoreOptions,
} from './watchQueryOptions';

import { Observable, Observer } from '../util/Observable';
import { Observable, Observer, Subscription } from '../util/Observable';

import {
QueryScheduler,
Expand Down Expand Up @@ -49,6 +50,7 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
private scheduler: QueryScheduler;
private queryManager: QueryManager;
private observers: Observer<ApolloQueryResult>[];
private subscriptionHandles: Subscription[];

private lastResult: ApolloQueryResult;
private lastError: ApolloError;
Expand Down Expand Up @@ -80,6 +82,7 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
this.queryId = queryId;
this.shouldSubscribe = shouldSubscribe;
this.observers = [];
this.subscriptionHandles = [];
}

public result(): Promise<ApolloQueryResult> {
Copy link
Contributor

Choose a reason for hiding this comment

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

When converting Observables to promises the convention is usually to take last value emitted, not first value emitted.
which means you should save last value emitted, and only when complete reach resolve it for the promise.
also, i would have a global util function to do that task, and then just call it here.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that should go as a comment on this PR, since that code was not modified at all. Perhaps open a new issue or a PR? But I'm not sure how following that convention will improve the result method.

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah you are right. (regarding commenting here)
it will be improved because:

  1. Observable to promise is something that alot of people do, there is a way to do it.
  2. it should be one logic used towards the whole project, doing it over and over is redundant.
    want me to open an issue for it and later on check that?

Expand Down Expand Up @@ -165,7 +168,8 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
const reducer = fetchMoreOptions.updateQuery;
const mapFn = (previousResult: any, { variables }: {variables: any }) => {

// TODO REFACTOR: reached max recursion depth (figuratively). Continue renaming to variables further down when we have time.
// TODO REF: reached max recursion depth (fig) when renaming queryVariables to variables.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think using abbreviations in comments is a good idea.

// Continue renaming to variables further down when we have time.
const queryVariables = variables;
return reducer(
previousResult, {
Expand All @@ -178,6 +182,49 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
});
}

// XXX the subscription variables are separate from the query variables.
// if you want to update subscription variables, right now you have to do that separately,
// and you can only do it by stopping the subscription and then subscribing again with new variables.
Copy link
Contributor

Choose a reason for hiding this comment

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

SGTM

public subscribeToMore(
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 it should return observable not just teardown function.
which means, wrap it with: return new Observable((observer) => {
observable.subscribe(map...., observer.error, observer.complete)
return () => ....
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about it but decided against it, because I think if people want access to the subscription, they should use the client.susbcribe method.

Copy link
Contributor

Choose a reason for hiding this comment

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

so the whole point of this function is to update the store so the orginal observable will get updates?

Copy link
Contributor

Choose a reason for hiding this comment

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

That's correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, that's pretty much it :)

options: SubscribeToMoreOptions,
): () => void {
const observable = this.queryManager.startGraphQLSubscription({
document: options.document,
variables: options.variables,
});

const reducer = options.updateQuery;

const subscription = observable.subscribe({
next: (subscriptionData) => {
const mapFn = (previousResult: Object, { variables }: { variables: Object }) => {
return reducer(
previousResult, {
subscriptionData,
variables,
}
);
};
this.updateQuery(mapFn);
},
error: (err) => {
// TODO implement something smart here when improving error handling
console.error(err);
},
});

this.subscriptionHandles.push(subscription);

return () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

If we later want to return more stuff like the errors, we will wish we didn't just return a function here.

Copy link
Contributor

@DxCx DxCx Oct 18, 2016

Choose a reason for hiding this comment

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

the convention is to return observable, then right after use observer.error(error)

// XXX technically we should also remove it from this.subscriptionHandles
const i = this.subscriptionHandles.indexOf(subscription);
if (i >= 0) {
this.subscriptionHandles.splice(i, 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the above comment about removing it out of date?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it is indeed

Copy link
Contributor

Choose a reason for hiding this comment

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

isn't splice removing it?
maybe there is a typo in the comment?

subscription.unsubscribe();
}
};
}

public setOptions(opts: ModifiableWatchQueryOptions): Promise<ApolloQueryResult> {
const oldOptions = this.options;
this.options = assign({}, this.options, opts) as WatchQueryOptions;
Expand Down Expand Up @@ -342,6 +389,9 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
this.scheduler.stopPollingQuery(this.queryId);
}

// stop all active GraphQL subscriptions
this.subscriptionHandles.forEach( sub => sub.unsubscribe() );
Copy link
Contributor

Choose a reason for hiding this comment

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

i would re-init subscriptionHandlers into an empty array at that point...
i mean, technicality, it is not needed as the teardown function cleans it.
but for people that are not familiar with observables i would add an explicit re-init
so it will be clear that it's empty at that point.


this.queryManager.stopQuery(this.queryId);
this.observers = [];
}
Expand Down
9 changes: 9 additions & 0 deletions src/core/watchQueryOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ export interface FetchMoreQueryOptions {
variables?: { [key: string]: any };
}

export type SubscribeToMoreOptions = {
document: Document;
variables?: { [key: string]: any };
updateQuery: (previousQueryResult: Object, options: {
subscriptionData: any,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we just wrap this in a data: ... to keep it consistent with fetchMore? I feel like that would be better than having two different but very similar APIs.

Copy link
Contributor

Choose a reason for hiding this comment

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

i agree with you that both should be consistent..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, I wasn't sure where to do it. I actually did that in another place, but I'd rather solve it in the subscriptions themselves. Will do the "patching" for now.

variables: { [key: string]: any },
}) => Object;
}

export interface DeprecatedSubscriptionOptions {
query: Document;
variables?: { [key: string]: any };
Expand Down
92 changes: 92 additions & 0 deletions test/subscribeToMore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as chai from 'chai';
const { assert } = chai;

import {
mockSubscriptionNetworkInterface,
} from './mocks/mockNetworkInterface';
import ApolloClient from '../src';

// import assign = require('lodash.assign');
// import clonedeep = require('lodash.clonedeep');

import gql from 'graphql-tag';

describe('subscribeToMore', () => {
const query = gql`
query aQuery {
entry {
value
}
}
`;
const result = {
data: {
entry: {
value: 1,
},
},
};

const req1 = { request: { query }, result };

const results = ['Dahivat Pandya', 'Amanda Liu'].map(
name => ({ result: { name: name }, delay: 10 })
);

const sub1 = {
request: {
query: gql`
subscription newValues {
name
}
`,
},
id: 0,
results: [...results],
};

it('triggers new result from subscription data', (done) => {
let latestResult: any = null;
const networkInterface = mockSubscriptionNetworkInterface([sub1], req1);
let counter = 0;

const client = new ApolloClient({
networkInterface,
addTypename: false,
});

const obsHandle = client.watchQuery({
query,
});
const sub = obsHandle.subscribe({
next(queryResult) {
latestResult = queryResult;
counter++;
},
});

obsHandle.subscribeToMore({
document: gql`
subscription newValues {
name
}
`,
updateQuery: (prev, { subscriptionData }) => {
return { entry: { value: subscriptionData.name } };
},
});

setTimeout(() => {
sub.unsubscribe();
assert.equal(counter, 3);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure exceptions will raise (and not just print) that way (setTimeout, annon func)
did you verify that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it worked alright.

Copy link
Contributor

Choose a reason for hiding this comment

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

cool, i have bad experience with timeouts and exceptions hehe..
better safe then sorry ;)

assert.deepEqual(latestResult, { data: { entry: { value: 'Amanda Liu' } }, loading: false });
done();
}, 50);

for (let i = 0; i < 2; i++) {
networkInterface.fireResult(0); // 0 is the id of the subscription for the NI
}
});

// TODO add a test that checks that subscriptions are cancelled when obs is unsubscribed from.
});
1 change: 1 addition & 0 deletions test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ import './mockNetworkInterface';
import './graphqlSubscriptions';
import './batchedNetworkInterface';
import './ObservableQuery';
import './subscribeToMore';