Making multiple socket instances in React with socket.io, each in a namespace - websocket

I have a nestjs application which has Websockets integrated with socket.io. Some of the gateways need authentication. So connecting to them without authenticating logs you out. The problem is, I need some of them without authentication, so I managed to figure out that I could use "namespaces" to connect only to specific Gateways.
I specified in the gateways the namespaces like this:
#WebSocketGateway({
namespace: 'tourneys',
...ConfigConstants.WsConfig,
})
export class AuxiliaryGateway
and in gateways that need authentication, I made it like this:
#UseGuards(SocketSessionGuard)
#WebSocketGateway({
namespace: 'matches',
...ConfigConstants.WsConfig,
})
The problem doesn't seem to be on the back-end however. In the front-end, I tried connecting the websockets like this:
import React, { useEffect, useMemo } from "react";
import { io, ManagerOptions, Socket, SocketOptions } from "socket.io-client";
import { SocketContext } from "#lib/context/SocketContext";
import {
ServerToClientEvents,
ClientToServerEvents,
} from "#lib/types/socket/instance";
import { getAuthToken } from "#lib/services/storage/authToken";
export const SocketProvider: React.FC = ({ children }) => {
const options = {
auth: {
token: getAuthToken(),
},
transports: ["websocket"],
timeout: 20000,
reconnectionAttempts: 10,
reconnectionDelay: 1500,
reconnectionDelayMax: 5000,
} as Partial<ManagerOptions & SocketOptions>;
const tourneysSocket: Socket<ServerToClientEvents, ClientToServerEvents> = io(
`${process.env.NEXT_PUBLIC_WSS_HOST}/tourneys `,
options
);
const matchesSocket: Socket<ServerToClientEvents, ClientToServerEvents> = io(
`${process.env.NEXT_PUBLIC_WSS_HOST}/matches `,
options
);
useEffect(() => {
tourneysSocket.on("connect", () => {
console.log("conectado");
});
tourneysSocket.on("disconnect", e => {
console.warn(`- desconectado "disconnect", ${e}`);
});
tourneysSocket.on("exception", e => {
console.error(e);
});
matchesSocket.on("connect", () => {
console.log("conectado");
});
matchesSocket.on("disconnect", e => {
console.warn(`- desconectado "disconnect", ${e}`);
});
matchesSocket.on("exception", e => {
console.error(e);
});
}, [tourneysSocket, matchesSocket]);
const value = useMemo(
() => ({
tourneysSocket,
matchesSocket,
}),
[tourneysSocket, matchesSocket]
);
return (
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
);
};
I make two instances, one for each namespace. However, these instances they stop emitting to the correct subscribes after some testing. What might be causing this issue? I can't figure out and I believe it's happening in the front-end. React somehow seems to not use the sockets I'm instatiating after some emits.

Related

There is no matching message handler error in NestJs TCP E2E test

I'm playing around with Microservice architecture using NestJs. I've made a simplified repository with a few services that communicate over TCP with a mix of message and event patterns.
I have moved on to writing E2E tests for the using Supertest, and while I'm able to run the needed microservice, the requests respond with {"error": "There is no matching message handler defined in the remote service.", "statusCode": 500}
GatewayService: HTTP Rest Api where the E2E tests are run. Calls the service
AuthService: NestJs microservice running on 0.0.0.0:3001 by default
configService: a simple service that returns information needed to set up the services, like host and port. I have tried eliminating it from the test and hardcoding the values.
The E2E test file
import { INestApplication, ValidationPipe } from '#nestjs/common';
import { ClientProxy, ClientsModule, Transport } from '#nestjs/microservices';
import { Test, TestingModule } from '#nestjs/testing';
import * as request from 'supertest';
import { configService } from '../src/config.service';
import { RpcExceptionFilter } from '../src/filters/rpc-exception.filter';
import { AppModule } from './../src/app.module';
describe('AuthenticationController (e2e)', () => {
let app: INestApplication;
let authClient: ClientProxy;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
AppModule,
ClientsModule.register([
{
...configService.getServiceConfigs().authService,
transport: Transport.TCP,
},
]),
],
}).compile();
// Setup the app instance
app = moduleFixture.createNestApplication();
// Setup the relevant micorservice(s)
app.connectMicroservice({
transport: Transport.TCP,
name: configService.getServiceConfigs().authService.name,
options: configService.getServiceConfigs().authService.options,
});
app.startAllMicroservices();
// Add request validation
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
}),
);
// Add needed filters
app.useGlobalFilters(new RpcExceptionFilter());
await app.init();
authClient = app.get(configService.getServiceConfigs().authService.name);
await authClient.connect();
console.log('authClient', authClient);
});
describe('POST /auth/login', () => {
it('Should return status 200 and a user object with access token', () => {
return (
request(app.getHttpServer())
.post('/auth/login')
.send({ username: 'exmple#user.com', password: 'password' })
// .expect(200)
.expect((response) => {
console.log('response', response.body);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('username');
expect(response.body).toHaveProperty('accessToken');
})
);
});
});
afterAll(async () => {
await app.close();
await authClient.close();
});
});
I have attempted adding a provider which I've used before when working with Grpc as the transport layer (this is TCP). Didn't change anything.
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
...
providers: [
{
provide: 'AUTH_SERVICE',
useFactory: () => {
return ClientProxyFactory.create({
transport: Transport.TCP,
options: { host: 'localhost', port: 3001 },
});
},
},
],
I know that the microservice starts up and the gateway service is able to connect to it since when printing the authClient: Client proxy it returns a correct object with URL 0.0.0.0:3001. If I change the URL, or the name of the service in any part of the setup then errors about missing providers show, further confirming that it is supposedly correctly set up.
One of the best guides I've found on this matter. Sadly it doesn't work for my code.

How to test nestjs with graphql by end to end?

In the test/posts/posts.e2e-spec.ts file
import { INestApplication } from '#nestjs/common';
import { TypeOrmModule } from '#nestjs/typeorm';
import { Test, TestingModule } from '#nestjs/testing';
import request = require('supertest');
import { PostsModule } from '../../src/posts/posts.module';
describe('Posts (e2e)', () => {
const posts = {
id: 1,
name: 'FirstPost #1',
};
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
...
}),
PostModule,
],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('post', () => {
it('should retrieve all post data', async () => {
request(app.getHttpServer())
.post('/graphql')
.send({
query:
`{findPosts() {
name
}}`,
})
.expect(200)
.expect((res) => {
console.log(res.body.data)
expect(res.body.data.post.length).toEqual(posts.length)
})
})
})
});
I created migration and inserted data into database first, then run this test, it can't go to the expect items. Even set console log I can't see anything in the output.
So maybe the /graphql can't be access in this way? I can access the endpoint from browser as http://localhost:3000/graphql.
If import supertest as
import * as request from 'supertest';
In the line request it showed:
This expression is not callable. Type ‘typeof supertest’ has no call signatures.
The version of them:
supertest: 6.1.3
#types/supertest: 2.0.11
Check out this very useful link https://github.com/jmcdo29/testing-nestjs/tree/main/apps/graphql-sample. It explains a lot of things regarding tests including graphql nestjs testing along with sample application

apollo-server-lambda: Unable to determine event source based on event

I am using apollo-server-lambda for my app. I have create custom authoization http headers and it is required . if authoization: LETMEIN then it will return true and also return all data, if there is no any authoization or wrong authoization then it wll throw an error. For local development I used serverless-offline.In Local environment, it works as expected and here is the image but when I deploy my code to AWS, the api end does not work. It always throws me the error: here is the link.
I test my function AWS console. I am getting this error:
I did not get what I am doing wrong.
Here is my code
/* eslint-disable #typescript-eslint/no-var-requires */
import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core';
import { ApolloServer, AuthenticationError } from 'apollo-server-lambda';
import schema from '../graphql/schema';
import resolvers from '../resolvers';
import runWarm from '../utils/run-warm';
export const authToken = (token: string) => {
if (token === 'LETMEIN') {
return;
} else {
throw new AuthenticationError('No authorization header supplied');
}
};
const server = new ApolloServer({
typeDefs: schema,
resolvers,
debug: false,
plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
context: ({ event }) => {
//console.log(context);
if (event.headers) {
authToken(event.headers.authorization);
}
},
});
export default runWarm(
server.createHandler({
expressGetMiddlewareOptions: {
cors: {
origin: '*',
credentials: true,
allowedHeaders: ['Content-Type', 'Origin', 'Accept'],
optionsSuccessStatus: 200,
maxAge: 200,
},
},
})
);
This is my Lambda function
/**
* Running warm functions help prevent cold starts
*/
const runWarm =
(lambdaFunc: AWSLambda.Handler): AWSLambda.Handler =>
(event, context, callback) => {
// Detect the keep-alive ping from CloudWatch and exit early. This keeps our
// lambda function running hot.
if (event.source === 'serverless-plugin-warmup') {
return callback(null, 'pinged');
}
return lambdaFunc(event, context, callback);
};
export default runWarm;
This is not a direct answer, but might help, and could be useful if anyone else (like me) found this thread because of the error "Unable to determine event source based on event" when using apollo-server-lambda.
That error is coming from #vendia/serverless-express which is being used by apollo-server-lambda.
Within serverless-express, in src/event-sources/utils.js, there is a function called getEventSourceNameBasedOnEvent(), which is throwing the error. It needs to find something in the event object, and after a bit of experimentation I found that writing the lambda function like this solved the issue for me:
const getHandler = (event, context) => {
const server = new ApolloServer({
typeDefs,
resolvers,
debug: true,
});
const graphqlHandler = server.createHandler();
if (!event.requestContext) {
event.requestContext = context;
}
return graphqlHandler(event, context);
}
exports.handler = getHandler;
Note that the context object is added to the event object with the key "requestContext"....that's the fix.
(Also note that I have defined typeDefs and resolvers elsewhere in the code)
I can't guarantee this is the ideal thing to do, but it did work for me.

Pubsub publish multiple events Apollo Server

I am using Apollo Server and I want to publish 2 events in the row from same resolver. Both subscriptions are working fine but only if I dispatch only one event. If I try to dispatch both, second subscription resolver never gets called. If I comment out the first event dispatch second works normally.
const publishMessageNotification = async (message, me, action) => {
const notification = await models.Notification.create({
ownerId: message.userId,
messageId: message.id,
userId: me.id,
action,
});
// if I comment out this one, second pubsub.publish starts firing
pubsub.publish(EVENTS.NOTIFICATION.CREATED, {
notificationCreated: { notification },
});
const unseenNotificationsCount = await models.Notification.find({
ownerId: notification.ownerId,
isSeen: false,
}).countDocuments();
console.log('unseenNotificationsCount', unseenNotificationsCount);// logs correct value
// this one is not working if first one is present
pubsub.publish(EVENTS.NOTIFICATION.NOT_SEEN_UPDATED, {
notSeenUpdated: unseenNotificationsCount,
});
};
I am using default pubsub implementation. There are no errors in the console.
import { PubSub } from 'apollo-server';
import * as MESSAGE_EVENTS from './message';
import * as NOTIFICATION_EVENTS from './notification';
export const EVENTS = {
MESSAGE: MESSAGE_EVENTS,
NOTIFICATION: NOTIFICATION_EVENTS,
};
export default new PubSub();
Make sure, that you use pubsub from context of apollo server, for example:
Server:
const server = new ApolloServer({
schema: schemaWithMiddleware,
subscriptions: {
path: PATH,
...subscriptionOptions,
},
context: http => ({
http,
pubsub,
redisCache,
}),
engine: {
apiKey: ENGINE_API_KEY,
schemaTag: process.env.NODE_ENV,
},
playground: process.env.NODE_ENV === 'DEV',
tracing: process.env.NODE_ENV === 'DEV',
debug: process.env.NODE_ENV === 'DEV',
});
and example use in resolver, by context:
...
const Mutation = {
async createOrder(parent, { input }, context) {
...
try {
...
context.pubsub.publish(CHANNEL_NAME, {
newMessage: {
messageCount: 0,
},
participants,
});
dialog.lastMessage = `{ "orderID": ${parentID}, "text": "created" }`;
context.pubsub.publish(NOTIFICATION_CHANNEL_NAME, {
notification: { messageCount: 0, dialogID: dialog.id },
participants,
});
...
}
return result;
} catch (err) {
log.error(err);
return sendError(err);
}
},
};
...
It has been a while since this moment.
I have also been a struggle with pubsub not working problem.
and I would like to see your ApolloClient setup code.
I changed my configurations with regard to graphql version and client-side setup.
graphql version : 14.xx.xx -> 15.3.0
const client = new ApolloClient({
uri: 'http://localhost:8001/graphql',
cache: cache,
credentials: 'include',
link: ApolloLink.from([wsLink, httpLink])
});
I want you to clarify link order, especially about httpLink, if you use in your case, "HttpLink is a terminating Link.", according to Apollo official site.
At first, I used link order [httpLink, wsLink].
Therefore, pubsub.publish didn't work.
I hope this answer will help some of graphql users.

Update method in mutation not running

I have the following component that mutates data. Apollo provides functionality to update the store automatically. I would like to control the way the data is added to the store using the update function. The documentation is straightforward enough, but I can't get it working. What is wrong in the code below that would prevent the console.log from printing.
import React from 'react'
import { connect } from 'react-redux';
import { graphql, gql, compose } from 'react-apollo';
import { personCodeSelector } from '../../selectors/auth';
import UploadBankStatement from '../../components/eftFileUploads/UploadBankStatement.jsx';
const createEftFileUpload = gql`mutation createEftFileUpload(
$bankAccountCode: String!,
$uploadInput: UploadInput!,
$uploadedByPersonCode: String!) {
createEftFileUpload(
bankAccountCode: $bankAccountCode,
uploadInput: $uploadInput,
uploadedByPersonCode: $uploadedByPersonCode) {
id
bankAccountCode
fileName
numberOfProcessedItems
numberOfUnallocatedItems
createdAt
status
}
}`;
const mutationConfig = {
props: ({ ownProps, mutate }) => ({
createEftFileUpload: (bankAccountCode, uploadInput) => {
return mutate({
variables: {
bankAccountCode,
uploadInput,
uploadedByPersonCode: ownProps.personCode
},
update: (store, something) => {
console.log("ping");
console.log(store, something);
},
});
}
})
};
const mapStateToProps = state => {
return {
personCode: personCodeSelector(state)
};
};
export default compose(
connect(mapStateToProps),
graphql(createEftFileUpload, mutationConfig)
)(UploadBankStatement);
Note I have found a couple of similar issues, but it doesn't seem to shed any light on my situation.
Server restart fix my issue. Not sure why this was required with hot-reloading. The code was correct.

Resources