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

Subscriptions support #1298

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"setupEnvScriptFile": "node_modules/fbjs-scripts/jest/environment.js",
"persistModuleRegistryBetweenSpecs": true,
"modulePathIgnorePatterns": [
"[.](.*).js",
"<rootDir>/lib/",
"<rootDir>/src/(.*).native.js",
"<rootDir>/node_modules/(?!(fbjs/lib/|react/lib/|fbjs-scripts/jest))"
Expand Down
2 changes: 2 additions & 0 deletions src/RelayPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const RelayContainer = require('RelayContainer');
const RelayEnvironment = require('RelayEnvironment');
const RelayInternals = require('RelayInternals');
const RelayMutation = require('RelayMutation');
const RelaySubscription = require('RelaySubscription');
const RelayPropTypes = require('RelayPropTypes');
const RelayQL = require('RelayQL');
const RelayReadyStateRenderer = require('RelayReadyStateRenderer');
Expand All @@ -39,6 +40,7 @@ if (typeof global.__REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined') {
const RelayPublic = {
Environment: RelayEnvironment,
Mutation: RelayMutation,
Subscription: RelaySubscription,
PropTypes: RelayPropTypes,
QL: RelayQL,
ReadyStateRenderer: RelayReadyStateRenderer,
Expand Down
1 change: 1 addition & 0 deletions src/container/RelayContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ function createContainerComponent(
hasPartialData: this.hasPartialData.bind(this),
pendingVariables: null,
route,
subscribe: this.context.relay.subscribe,
setVariables: this.setVariables.bind(this),
variables: {},
},
Expand Down
17 changes: 17 additions & 0 deletions src/container/__tests__/RelayContainer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const ReactTestUtils = require('ReactTestUtils');
const Relay = require('Relay');
const RelayEnvironment = require('RelayEnvironment');
const RelayMutation = require('RelayMutation');
const RelaySubscription = require('RelaySubscription');
const RelayQuery = require('RelayQuery');
const RelayRoute = require('RelayRoute');
const RelayTestUtils = require('RelayTestUtils');
Expand Down Expand Up @@ -563,6 +564,22 @@ describe('RelayContainer', function() {
});
});

describe('props.relay.subscribe', () => {
it('forwards to the underlying RelayEnvironment', () => {
const mockSubscription = new RelaySubscription();
environment.subscribe = jest.fn();
render.mockImplementation(function() {
this.props.relay.subscribe(mockSubscription);
});
RelayTestRenderer.render(
() => <MockContainer />,
environment,
mockRoute
);
expect(environment.subscribe.mock.calls[0][0]).toBe(mockSubscription);
Copy link
Contributor

Choose a reason for hiding this comment

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

expect(environment.subscribe).toBeCalledWith(mockSubscription);
Not super important but it simplifies a little

Copy link
Author

Choose a reason for hiding this comment

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

👍

});
});

it('creates resolvers for each query prop with a fragment pointer', () => {
RelayTestRenderer.render(
() => <MockContainer foo={mockFooPointer} />,
Expand Down
12 changes: 12 additions & 0 deletions src/network-layer/default/RelayDefaultNetworkLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ const RelayMutationRequest = require('RelayMutationRequest');

const fetch = require('fetch');
const fetchWithRetries = require('fetchWithRetries');
const invariant = require('invariant');

import type {InitWithRetries} from 'fetchWithRetries';
import type RelayQueryRequest from 'RelayQueryRequest';
import type RelaySubscriptionRequest from 'RelaySubscriptionRequest';
import type {Subscription} from 'RelayTypes';

type GraphQLError = {
message: string,
Expand All @@ -40,6 +43,7 @@ class RelayDefaultNetworkLayer {
// Facilitate reuse when creating custom network layers.
(this: any).sendMutation = this.sendMutation.bind(this);
(this: any).sendQueries = this.sendQueries.bind(this);
(this: any).sendSubscription = this.sendSubscription.bind(this);
(this: any).supports = this.supports.bind(this);
}

Expand Down Expand Up @@ -80,6 +84,14 @@ class RelayDefaultNetworkLayer {
)));
}

sendSubscription(request: RelaySubscriptionRequest): Subscription {
invariant(
false,
'RelayDefaultNetworkLayer: `sendSubscription` is not implemented in the ' +
'default network layer. A custom network layer must be injected.'
);
}

supports(...options: Array<string>): boolean {
// Does not support the only defined option, "defer".
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const RelayConnectionInterface = require('RelayConnectionInterface');
const RelayDefaultNetworkLayer = require('RelayDefaultNetworkLayer');
const RelayMetaRoute = require('RelayMetaRoute');
const RelayMutationRequest = require('RelayMutationRequest');
const RelaySubscriptionRequest = require('RelaySubscriptionRequest');
const RelayQuery = require('RelayQuery');
const RelayQueryRequest = require('RelayQueryRequest');
const RelayTestUtils = require('RelayTestUtils');
Expand Down Expand Up @@ -412,4 +413,49 @@ describe('RelayDefaultNetworkLayer', () => {
);
});
});

describe('sendSubscription', () => {
let mockSubscriptionRequest;

beforeEach(() => {
const initialVariables = {feedbackId: 'aFeedbackId'};
function makeMockSubscription() {
class MockSubscriptionClass extends Relay.Subscription {
static initialVariables = initialVariables;
getConfigs() {
return [];
}
getSubscription() {
return Relay.QL`
subscription {
feedbackLikeSubscribe (input: $input) {
feedback {
likeSentence
}
}
}
`;
}
getVariables() {
return initialVariables;
}
}
return MockSubscriptionClass;
}
const MockSubscription = makeMockSubscription();
const mockSubscription = new MockSubscription();
mockSubscriptionRequest = new RelaySubscriptionRequest(mockSubscription, {
onNext: jest.fn(),
onError: jest.fn(),
onCompleted: jest.fn(),
});
});

it('should throw "not implemented" invariant error', () => {
expect(() => networkLayer.sendSubscription(mockSubscriptionRequest)).toFailInvariant(
'RelayDefaultNetworkLayer: `sendSubscription` is not implemented in the ' +
'default network layer. A custom network layer must be injected.'
);
});
});
});
24 changes: 24 additions & 0 deletions src/network/RelayNetworkLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
'use strict';

import type RelayMutationRequest from 'RelayMutationRequest';
import type RelaySubscriptionRequest from 'RelaySubscriptionRequest';
const RelayProfiler = require('RelayProfiler');
import type RelayQuery from 'RelayQuery';
const RelayQueryRequest = require('RelayQueryRequest');
import type {ChangeSubscription, NetworkLayer} from 'RelayTypes';
import type {Subscription} from 'RelayTypes';

const invariant = require('invariant');
const resolveImmediate = require('resolveImmediate');
Expand Down Expand Up @@ -114,6 +116,28 @@ class RelayNetworkLayer {
}
}

sendSubscription(subscriptionRequest: RelaySubscriptionRequest): Subscription {
const implementation = this._getImplementation();

invariant(
typeof implementation.sendSubscription === 'function',
'RelayNetworkLayer: does not support subscriptions. Expected `sendSubscription` to be ' +
'a function.'
);

const result = implementation.sendSubscription(subscriptionRequest);

invariant(
result && typeof result.dispose === 'function',
'RelayNetworkLayer: `sendSubscription` should return an object with a ' +
'`dispose` property that is a no-argument function. This function is ' +
'called when the client unsubscribes from the subscription ' +
'and any network layer resources can be cleaned up.'
);

return result;
}

supports(...options: Array<string>): boolean {
const implementation = this._getImplementation();
return implementation.supports(...options);
Expand Down
116 changes: 116 additions & 0 deletions src/network/RelaySubscriptionRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule RelaySubscriptionRequest
* @flow
*/

'use strict';

import type {PrintedQuery} from 'RelayInternalTypes';
import type RelayQuery from 'RelayQuery';
import type {
RelaySubscriptionObservableCallbacks,
SubscriptionResult,
Variables,
} from 'RelayTypes';

const printRelayQuery = require('printRelayQuery');

/**
* @internal
*
* Instances of these are made available via `RelayNetworkLayer.sendSubscription`.
*/
class RelaySubscriptionRequest {
_subscription: RelayQuery.Subscription;
_callbacks: RelaySubscriptionObservableCallbacks;
_printedQuery: ?PrintedQuery;

constructor(
subscription: RelayQuery.Subscription,
callbacks: RelaySubscriptionObservableCallbacks,
) {
this._subscription = subscription;
this._callbacks = callbacks;
}

/**
* @public
*
* Gets a string name used to refer to this request for printing debug output.
*/
getDebugName(): string {
return this._subscription.getName();
}

/**
* @public
*
* Gets the variables used by the subscription. These variables should be
* serialized and sent in the GraphQL request.
*/
getVariables(): Variables {
return this._getPrintedQuery().variables;
}

/**
* @public
*
* Gets a string representation of the GraphQL subscription.
*/
getQueryString(): string {
return this._getPrintedQuery().text;
}

/**
* @public
* @unstable
*/
getSubscription(): RelayQuery.Subscription {
return this._subscription;
}

/**
* @public
* @unstable
*/
onCompleted(): void {
return this._callbacks && this._callbacks.onCompleted();
}

/**
* @public
* @unstable
*/
onNext(payload: SubscriptionResult): void {
return this._callbacks && this._callbacks.onNext(payload);
}

/**
* @public
* @unstable
*/
onError(error: Error): void {
return this._callbacks && this._callbacks.onError(error);
}

/**
* @private
*
* Returns the memoized printed query.
*/
_getPrintedQuery(): PrintedQuery {
if (!this._printedQuery) {
this._printedQuery = printRelayQuery(this._subscription);
}
return this._printedQuery;
}
}

module.exports = RelaySubscriptionRequest;
55 changes: 55 additions & 0 deletions src/network/__tests__/RelayNetworkLayer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('RelayNetworkLayer', () => {
injectedNetworkLayer = {
sendMutation: jest.fn(),
sendQueries: jest.fn(),
sendSubscription: jest.fn(),
supports: jest.fn(() => true),
};
networkLayer = new RelayNetworkLayer();
Expand Down Expand Up @@ -204,6 +205,60 @@ describe('RelayNetworkLayer', () => {
});
});

describe('sendSubscription', () => {
let subscriptionRequest;
beforeEach(() => {
subscriptionRequest = {};
});

it('should call network layer does sendSubscription', () => {
injectedNetworkLayer.sendSubscription.mockReturnValue({ dispose: jest.fn() });
networkLayer.sendSubscription(subscriptionRequest);

expect(injectedNetworkLayer.sendSubscription).toBeCalled();
expect(injectedNetworkLayer.sendSubscription.mock.calls.length).toBe(1);
expect(injectedNetworkLayer.sendSubscription).toBeCalledWith(subscriptionRequest);
});

it('throws error when network layer sendSubscription does not returns nothing', () => {
injectedNetworkLayer.sendSubscription.mockReturnValue(undefined);

expect(() => networkLayer.sendSubscription(subscriptionRequest)).toFailInvariant(
'RelayNetworkLayer: `sendSubscription` should return an object with a ' +
'`dispose` property that is a no-argument function. This function is ' +
'called when the client unsubscribes from the subscription ' +
'and any network layer resources can be cleaned up.'
);
});

it('throws error when network layer sendSubscription does not return a disposable', () => {
injectedNetworkLayer.sendSubscription.mockReturnValue({});

expect(() => networkLayer.sendSubscription(subscriptionRequest)).toFailInvariant(
'RelayNetworkLayer: `sendSubscription` should return an object with a ' +
'`dispose` property that is a no-argument function. This function is ' +
'called when the client unsubscribes from the subscription ' +
'and any network layer resources can be cleaned up.'
);
});

describe('injected network layer does not have sendSubscription', () => {
beforeEach(() => {
networkLayer.injectImplementation({
...injectedNetworkLayer,
sendSubscription: undefined,
});
});

it('throws when network layer does not have sendSubscription', () => {
expect(() => networkLayer.sendSubscription(subscriptionRequest)).toFailInvariant(
'RelayNetworkLayer: does not support subscriptions. Expected `sendSubscription` to be ' +
'a function.'
);
});
});
});

describe('addNetworkSubscriber', () => {
let mutationCallback;
let queryCallback;
Expand Down
Loading