Relay Modern: Connecting websocket to network layer - reactjs

I’m having issues figuring out how to connect the Relay Modern network layer with my websocket instance.
I’m currently instantiating a websocket instance as:
const subscriptionWebSocket = new ReconnectingWebSocket('ws://url.url/ws/subscriptions/', null, options);
I'm specifying the subscription and creating a new instance of requestSubscription:
const subscription = graphql`
subscription mainSubscription {
testData {
anotherNode {
data
}
}
}
`;
requestSubscription(
environment,
{
subscription,
variables: {},
onComplete: () => {...},
onError: (error) => {...},
onNext: (response) => {...},
updater: (updaterStoreConfig) => {...},
},
);
Which then allows me to send any subscription requests:
function subscriptionHandler(subscriptionConfig, variables, cacheConfig, observer) {
subscriptionWebSocket.send(JSON.stringify(subscriptionConfig.text));
return {
dispose: () => {
console.log('subscriptionHandler: Disposing subscription');
},
};
}
const network = Network.create(fetchQuery, subscriptionHandler);
through to my server (currently using Graphene-python), and I’m able to interpret the received message on the server.
However, what I’m having issues figuring out is how to respond to a subscription; for example, when something changes in my DB, I want to generate a response and return to any potential subscribers.
The question being, how do I connect the onMessage event from my websocket instance into my Relay Modern Network Layer? I've browsed through the source for relay but can't seem to figure out what callback, or what method should be implementing an onreceive.
Any tips are appreciated.

I've managed to make subscriptions with Relay Modern work as well and wanted to share my minimal setup, maybe it's helpful for someone!
Note that I'm not using WebSocket but the SubscriptionClient that can be found in subscriptions-transport-ws to manage the connection to the server.
Here's my minimal setup code:
Environment.js
import { SubscriptionClient } from 'subscriptions-transport-ws'
const {
Environment,
Network,
RecordSource,
Store,
} = require('relay-runtime')
const store = new Store(new RecordSource())
const fetchQuery = (operation, variables) => {
return fetch('https://api.graph.cool/relay/v1/__GRAPHCOOL_PROJECT_ID__', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: operation.text,
variables,
}),
}).then(response => {
return response.json()
})
}
const websocketURL = 'wss://subscriptions.graph.cool/v1/__GRAPHCOOL_PROJECT_ID__'
function setupSubscription(
config,
variables,
cacheConfig,
observer,
) {
const query = config.text
const subscriptionClient = new SubscriptionClient(websocketURL, {reconnect: true})
const id = subscriptionClient.subscribe({query, variables}, (error, result) => {
observer.onNext({data: result})
})
}
const network = Network.create(fetchQuery, setupSubscription)
const environment = new Environment({
network,
store,
})
export default environment
NewLinkSubscription.js
import {
graphql,
requestSubscription
} from 'react-relay'
import environment from '../Environment'
const newLinkSubscription = graphql`
subscription NewLinkSubscription {
Link {
mutation
node {
id
description
url
createdAt
postedBy {
id
name
}
}
}
}
`
export default (onNext, onError, onCompleted, updater) => {
const subscriptionConfig = {
subscription: newLinkSubscription,
variables: {},
onError,
onNext,
onCompleted,
updater
}
requestSubscription(
environment,
subscriptionConfig
)
}
Now you can simply use the exported function to subscribe. For example, in one of my React components in componentDidMount I can now do the following:
componentDidMount() {
NewLinkSubscription(
response => console.log(`Received data: `, response),
error => console.log(`An error occurred:`, error),
() => console.log(`Completed`)
)
}
Note that the SubscriptionClient can only be used if your server implements this protocol!
If you want to learn more, check out the fullstack How to GraphQL tutorial that describes in detail how to make subscriptions work with Relay Modern.

I’ll just write down how I’ve approached this issue after the assistance found in this thread. It might be usable for someone else. This is very dependent on the server-side solution that you've chosen.
My approach:
Firstly I built a SubscriptionHandler that will handle the requestStream#subscribeFunction through SubscriptionHandler#setupSubscription.
The SubscriptionHandler instantiates a WebSocket (using a custom version of ReconnectingWebSockets) and attaches the onmessage event to an internal method (SubscriptionHandler#receiveSubscriptionPayload) which will add the payload to the corresponding request.
We create new subscriptions through SubscriptionHandler#newSubscription which will use the internal attribute SubscriptionHandler.subscriptions to add a keyed entry of this subscription (we use an MD5-hash util over the query and variables); meaning the object will come out as:
SubscriptionHandler.subscriptions = {
[md5hash]: {
query: QueryObject,
variables: SubscriptionVariables,
observer: Observer (contains OnNext method)
}
Whenever the server sends a subscription response the SubscriptionHandler#receiveSubscriptionPayload method will be called and it will identify what subscription the payload belongs to by using the query/variables md5 hash, then use the SubscriptionHandler.subscriptions observer onNext method.
This approach requires the server to return a message such as:
export type ServerResponseMessageParsed = {
payload: QueryPayload,
request: {
query: string,
variables: Object,
}
}
I do not know if this is a great way of handling subscriptions, but it works for now with my current setup.
SubscriptionHandler.js
class SubscriptionHandler {
subscriptions: Object;
subscriptionEnvironment: RelayModernEnvironment;
websocket: Object;
/**
* The SubscriptionHandler constructor. Will setup a new websocket and bind
* it to internal method to handle receving messages from the ws server.
*
* #param {string} websocketUrl - The WebSocket URL to listen to.
* #param {Object} webSocketSettings - The options object.
* See ReconnectingWebSocket.
*/
constructor(websocketUrl: string, webSocketSettings: WebSocketSettings) {
// All subscription hashes and objects will be stored in the
// this.subscriptions attribute on the subscription handler.
this.subscriptions = {};
// Store the given environment internally to be reused when registering new
// subscriptions. This is required as per the requestRelaySubscription spec
// (method requestSubscription).
this.subscriptionEnvironment = null;
// Create a new WebSocket instance to be able to receive messages on the
// given URL. Always opt for default protocol for the RWS, second arg.
this.websocket = new ReconnectingWebSocket(
websocketUrl,
null, // Protocol.
webSocketSettings,
);
// Bind an internal method to handle incoming messages from the websocket.
this.websocket.onmessage = this.receiveSubscriptionPayload;
}
/**
* Method to attach the Relay Environment to the subscription handler.
* This is required as the Network needs to be instantiated with the
* SubscriptionHandler's methods, and the Environment needs the Network Layer.
*
* #param {Object} environment - The apps environment.
*/
attachEnvironment = (environment: RelayModernEnvironment) => {
this.subscriptionEnvironment = environment;
}
/**
* Generates a hash from a given query and variable pair. The method
* used is a recreatable MD5 hash, which is used as a 'key' for the given
* subscription. Using the MD5 hash we can identify what subscription is valid
* based on the query/variable given from the server.
*
* #param {string} query - A string representation of the subscription.
* #param {Object} variables - An object containing all variables used
* in the query.
* #return {string} The MD5 hash of the query and variables.
*/
getHash = (query: string, variables: HashVariables) => {
const queryString = query.replace(/\s+/gm, '');
const variablesString = JSON.stringify(variables);
const hash = md5(queryString + variablesString).toString();
return hash;
}
/**
* Method to be bound to the class websocket instance. The method will be
* called each time the WebSocket receives a message on the subscribed URL
* (see this.websocket options).
*
* #param {string} message - The message received from the websocket.
*/
receiveSubscriptionPayload = (message: ServerResponseMessage) => {
const response: ServerResponseMessageParsed = JSON.parse(message.data);
const { query, variables } = response.request;
const hash = this.getHash(query, variables);
// Fetch the subscription instance from the subscription handlers stored
// subscriptions.
const subscription = this.subscriptions[hash];
if (subscription) {
// Execute the onNext method with the received payload after validating
// that the received hash is currently stored. If a diff occurs, meaning
// no hash is stored for the received response, ignore the execution.
subscription.observer.onNext(response.payload);
} else {
console.warn(Received payload for unregistered hash: ${hash});
}
}
/**
* Method to generate new subscriptions that will be bound to the
* SubscriptionHandler's environment and will be stored internally in the
* instantiated handler object.
*
* #param {string} subscriptionQuery - The query to subscribe to. Needs to
* be a validated subscription type.
* #param {Object} variables - The variables for the passed query.
* #param {Object} configs - A subscription configuration. If
* override is required.
*/
newSubscription = (
subscriptionQuery: GraphQLTaggedNode,
variables: Variables,
configs: GraphQLSubscriptionConfig,
) => {
const config = configs || DEFAULT_CONFIG;
requestSubscription(
this.subscriptionEnvironment,
{
subscription: subscriptionQuery,
variables: {},
...config,
},
);
}
setupSubscription = (
config: ConcreteBatch,
variables: Variables,
cacheConfig: ?CacheConfig,
observer: Observer,
) => {
const query = config.text;
// Get the hash from the given subscriptionQuery and variables. Used to
// identify this specific subscription.
const hash = this.getHash(query, variables);
// Store the newly created subscription request internally to be re-used
// upon message receival or local data updates.
this.subscriptions[hash] = { query, variables };
const subscription = this.subscriptions[hash];
subscription.observer = observer;
// Temp fix to avoid WS Connection state.
setTimeout(() => {
this.websocket.send(JSON.stringify({ query, variables }));
}, 100);
}
}
const subscriptionHandler = new SubscriptionHandler(WS_URL, WS_OPTIONS);
export default subscriptionHandler;

For anyone stumbling across this recently, I did not have success with either the solutions above because of recent updates in the libraries involved. Yet they were a great source to start and I put up together a small example based on the official relay modern todo example, it is very minimalistic and uses helpers libraries from Apollo but works well with relay modern:
https://github.com/jeremy-colin/relay-examples-subscription
It includes both server and client
Hope it can help

I think this repo would fit your needs.
Helps you creating your subscriptions server-side

Related

How to add a live counter in react that displays a firestore collection value securely?

Goal: To have a live counter on my React Firebase site that displays the current number of views on my youtube channel.
Method: I am retrieving the view count by calling the Youtube API from a firebase cloud function so that I can protect my API key. Once I have the data, I use another cloud function to update a view count collection in my firestore database (using pubsub every 60 minutes). I then monitor this collection using onSnapshot in my front-end and display the value.
Problem: While this does work, I get an email every day from Firebase saying my firestore database "has insecure rules" because "any user can read the entire database." I do not want to add authentication to my site, but I would like to have the live counter fully secure. Does anybody know how to have a live counter referencing a firestore database?
Front-end code:
export default function Navbar() {
const [count, setCount] = React.useState('');
const unsub = onSnapshot(doc(db, "viewCount", "Count"), (doc) => {
setCount(doc.get("View Count"));
});
Firestore rules:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if true;
allow write: if false;
}
}
}
Cloud functions
/**
* Function updates Firestore view count every 60 minutes
*
* #param -
* #returns - N/A
*/
exports.updateViewCount = functions.pubsub
.schedule("every 60 minutes")
.onRun((context) => {
try {
this.logCount(5);
console.log("Count updated at:", new Date());
} catch (error) {
console.error("Error getting view count: ", error);
}
});
/**
* Function calls getViewCount to get Youtube View Count
*
* #param -
* #returns - Int: View count
*/
exports.logCount = functions
.runWith({
secrets: ["YOUTUBE_API"],
})
.https.onCall(async (data, context) => {
const viewData = await getViewCount(
process.env.YOUTUBE_API,
process.env.YOUTUBE_CHANNEL_ID
);
const addData = await admin
.firestore()
.collection("viewCount")
.doc("Count")
.set({ "View Count": viewData })
.then(() => {
return viewData;
})
.catch((error) => {
console.error("Error writing document: ", error);
});
});
To recap:
Is this the right way to implement a live counter with firestore without authentication?
If so: How can I do this more securely? If not: what other options are there?
To get the warning emails to stop, you just need to change your rules so that allow read: if true; isn't applied to the entire database. You can still apply it to the part of the database where the counter is.
service cloud.firestore {
match /databases/{database}/documents {
match /viewCount/Count {
allow read: if true;
allow write: if false;
}
}
}
If you have other documents and collections that are being used by your app, you'll need to add rules for them too.

Trying to Call Google Cloud Function, Where Do I Put The Function URL And /path/to/credentials.json in CloudFunctionsServiceClient?

I copied this code from https://cloud.google.com/nodejs/docs/reference/functions/latest/functions/v1.cloudfunctionsserviceclient:
/**
* TODO(developer): Uncomment these variables before running the sample.
*/
/**
* Required. The name of the function to be called.
*/
// const name = 'abc123'
/**
* Required. Input to be passed to the function.
*/
// const data = 'abc123'
// Imports the Functions library
const {CloudFunctionsServiceClient} = require('#google-cloud/functions').v1;
// Instantiates a client
const functionsClient = new CloudFunctionsServiceClient();
async function callCallFunction() {
// Construct request
const request = {
name,
data,
};
// Run request
const response = await functionsClient.callFunction(request);
console.log(response);
}
callCallFunction();
This doesn't help me that much. I have a cloud function (in Python) that simply prints "hello world" or something simple like that. My cloud function can only be run through a "service account" that I created and I downloaded the .json file containing my credentials for this service account.
I'm making a Next.js app (with typescript) and I want to call this function in the app. So keeping the above example in mind where do I put these variables?
https://us-central1-<projectname>.cloudfunctions.net/<functionname>
/path/to/credentials.json
You can use your credentials.json by passing it on the constructor. Also you won't be needing to explicitly call the URL of the function. Properly defining the function in name should tell your code to use the function.
Using your code, I modified it to include the use of the credentials.json and defining of name. You can check this reference to construct the name properly.
const project = 'your-project' // update with your project name
const location = 'us-central1' //update this with the actual location of your function
const func_name = 'function-1' // update with your function name
const name = `projects/${project}/locations/${location}/functions/${func_name}`
const keyFilename = '/credentials.json' // update with the full path of your json
const data = '{"message":"test"}' // update with your request body
// Imports the Functions library
const {CloudFunctionsServiceClient} = require('#google-cloud/functions').v1;
// Instantiates a client
const options = {keyFilename};
const functionsClient = new CloudFunctionsServiceClient(options);
async function callCallFunction() {
// Construct request
const request = {
name,
data,
};
// Run request
const response = await functionsClient.callFunction(request);
console.log(response);
}
callCallFunction();
Test run:

Delaying Intercept responses in Cypress

I am writing cypress tests, and I want to test a feature of our program which will display data as it begins to appear. This could be a list with several hundred elements, and in order to speed it up, we break the request up into multiple requests.
The first request will fetch the first 50 items, the second request the next 100, etc.
I want to ensure that after the first request returns, the list contains 50 items, and that after the second request returns, the list contains 150 items.
What I don't know how to do, is reliably delay the intercept responses so that I can return the first 50, check the list length, then allow more data to come in.
This is what I have so far:
const baseUrl = 'https://example.io/listData*'
const document = 'public_document_10101'
cy.intercept(
{
url: baseUrl,
query: {
limit: '50',
'document': document
},
},
{ fixture: 'first-response.js' }
).as('firstResponse')
cy.intercept(
{
url: baseUrl,
query: {
limit: '100',
skip: '50',
'document': document
},
},
{ fixture: 'second-response.js' }
).as('secondResponse')
I don't believe that something like cy.wait('#firstResponse') will do what I want, as this doesn't prevent the second one from return at all. I'm open to using a delay of some kind, however it would be really nice if there was a wait to completely block it until I say so.
You can also use setDelay. Cypress Docs
cy.intercept({ url: 'http://localhost:3001/**', middleware: true }, (req) => {
req.on('response', (res) => {
// Wait for delay in milliseconds before sending the response to the client.
res.setDelay(1000)
})
})
This looks like the functionality you want Throttle or delay response all incoming responses
// Code from Real World App (RWA)
// cypress/support/index.ts
import { isMobile } from './utils'
import './commands'
// Throttle API responses for mobile testing to simulate real world conditions
if (isMobile()) {
cy.intercept({ url: 'http://localhost:3001/**', middleware: true }, (req) => {
req.on('response', (res) => {
// Throttle the response to 1 Mbps to simulate a mobile 3G connection
res.setThrottle(1000)
})
})
}
Or maybe Static Response object with fixture property and delay property
{
/**
* Serve a fixture as the response body.
*/
fixture?: string
/**
* Serve a static string/JSON object as the response body.
*/
body?: string | object | object[]
/**
* HTTP headers to accompany the response.
* #default {}
*/
headers?: { [key: string]: string }
/**
* The HTTP status code to send.
* #default 200
*/
statusCode?: number
/**
* If 'forceNetworkError' is truthy, Cypress will destroy the browser connection
* and send no response. Useful for simulating a server that is not reachable.
* Must not be set in combination with other options.
*/
forceNetworkError?: boolean
/**
* Milliseconds to delay before the response is sent.
*/
delay?: number
/**
* Kilobits per second to send 'body'.
*/
throttleKbps?: number
}

Firebase function not executed

import * as functions from 'firebase-functions';
console.log('I am a log entry0!');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
console.log('I am a log entry1!');
export const onMessageCreate = functions.database
.ref('/users/{userId}/totalScore')
.onUpdate((change) => {
console.log('I am a log entry2!');
//var a = admin.firestore().collection('/users');
})
I have deployed the function and I can see it in the console. But the function is not executed when totalScore is updated in the database....
Your database is Firestore but you use a Cloud Function that is triggered by an update in the Realtime Database. These are two different Firebase services and you need to change your code accordingly.
The following code will work:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.onMessageCreate = functions.firestore
.document('users/{userId}')
.onUpdate((change, context) => {
// Get an object representing the document
const newValue = change.after.data();
// ...or the previous value before this update
const previousValue = change.before.data();
if (newValue.totalScore !== previousValue.totalScore) {
console.log('NEW TOTAL SCORE');
}
return null;
//I guess you are going to do more than just logging to the console.
//If you do any asynchronous action, you should return the corresponding promise, see point 3 below
//For example:
//if (newValue.totalScore !== previousValue.totalScore) {
// return db.collection("score").doc(newValue.name).set({score: newValue.totalScore});
//}
});
Note that:
You cannot trigger the onUpdate Cloud Function when a specific field of the document changes. The Cloud Function will be triggered when any field of the Firestore document changes. But you can detect which field(s) have changed, as shown in the above code.
Since version 1.0 you have to initialize with admin.initializeApp();, see https://firebase.google.com/docs/functions/beta-v1-diffList
You need to indicate to the platform when the Cloud Function has finished executing: Since you are not executing any asynchronous operation in your Cloud Function you can use return null;. (For more details on this point, I would suggest you watch the 3 videos about "JavaScript Promises" from the Firebase video series: https://firebase.google.com/docs/functions/video-series/).
I think the update is checked on the ref not on the child
Try this
export const onMessageCreate = functions.database
.ref('/users/{userId}')
.onUpdate((change) => {
console.log('I am a log entry2!');
//var a = admin.firestore().collection('/users');
})
You get the old and new values of the snapshot at that location
If you are using Cloud Firestore then your listener is incorrect. In your case, you are specifying a listener for Realtime Database. We extract firestore from the functions and specify the path to the document we want to have a listener on:
import * as functions from 'firebase-functions';
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
export const onMessageCreate = functions.firestore
.document('users/{userId}')
.onUpdate((change, context) => {
console.log(change.before.data()); // shows the document before update
console.log(change.after.data()); // shows the document after change
return;
})

FeathersJS custom API method that retrieves enum Type, to fill a Dropdown in React

So I'm trying to fill a select component with a enum type from mongoose
In my user service the schema looks something like :
firstName: { type:String, required: true },
...
ris:{type: String, default: 'R', enum:['R', 'I', 'S']},
In my feathers service I can access the Model with "this.Model"
so in any hook I can do:
this.Model.schema.path('ris').enumValues); //['R','C','I']
and I get the values from the enum type.
Now since I can't create custom API methods other that the officials ones
Feathers calling custom API method
https://docs.feathersjs.com/clients/readme.html#caveats
https://docs.feathersjs.com/help/faq.html#can-i-expose-custom-service-methods
How can I create a service method/call/something so that I can call it in my
componentDidMount(){ var optns= this.props.getMyEnumsFromFeathers}
and have the enum ['R','C','I'] to setup my dropdown
I'm Using React/Redux/ReduxSaga-FeathersJS
I'd create a service for listing Enums in the find method:
class EnumService {
find(params) {
const { service, path } = params.query;
const values = this.app.service(service).Model.schema.path(path).enumValues;
return Promise.resolve(values);
}
setup(app) {
this.app = app;
}
}
app.use('/enums', new EnumService())
Then on the client you can do
app.service('enums').find({ query: {
service: 'myservice',
path: 'ris'
}
}).then(value => console.log('Got ', values));
I was trying to use this code, but, it does not work like plug and play.
after some play with the app service I figured out the code below
async find(params) {
const { service, path } = params.query;
const values = await this.app.service(service).Model.attributes[path].values;
return values || [];
}
setup(app) {
this.app = app;
}
I am not sure if it is a thing of what database is been used, in my case I am in development environment, so, I am using sqlite.

Resources