How to setup a WebSocket server inside SvelteKit using the ws package? - node.js

I'm trying to create a simple WebSocket server that will run in a SvelteKit application. I found this tutorial online which shows how to do it using Socket.io, however I would like to use the ws module instead.
This is the vite.config.ts file that I've come up with so far:
import type { UserConfig } from 'vite';
import { sveltekit } from '#sveltejs/kit/vite';
import { WebSocketServer } from "ws";
const webSocketServer = {
name: "webSocketServer",
configureServer: () => {
const webSocketServer = new WebSocketServer({
port: 8080
});
webSocketServer.on("connection", (socket, request) => {
socket.on("message", (data, isBinary) => {
console.log(`Recieved ${data}`);
});
socket.send("test from server");
});
}
}
const config: UserConfig = {
plugins: [sveltekit(), webSocketServer]
};
export default config;
I can connect to this WebSocket server from the frontend (in +page.svelte, for example) and send messages between them.
But every time I make a change to my server's code and save the file, I get an error saying that "there's already something listening on port 8080" and my Vite dev server terminates. If I then start back up my Vite dev server by running npm run dev then it will all work fine again, until I make any change to the WebSocket server code and save the file, and then the same thing will repeat.
I would rather not have to re-start my Vite dev server every time I make a change to my WebSocket server's code. If possible, I would rather Vite automatically shut down my old webSocket server to make room for the new one before booting it up each time so that they won't conflict.
The tutorial I linked above shows this code snippet:
import adapter from '#sveltejs/adapter-node'
import preprocess from 'svelte-preprocess'
import { Server } from 'socket.io'
const webSocketServer = {
name: 'webSocketServer',
configureServer(server) {
const io = new Server(server.httpServer)
io.on('connection', (socket) => {
socket.emit('eventFromServer', 'Hello, World 👋')
})
},
}
/** #type {import('#sveltejs/kit').Config} */
const config = {
preprocess: preprocess(),
kit: {
adapter: adapter(),
vite: {
plugins: [webSocketServer],
},
},
}
export default config
This snippet is using an old SvelteKit version where Vite configuration was done inside the svelte.config.js file, so the layout is a little different, but it seems they're simply spinning up their Socket.io server directly inside the configureServer method just like I am, but they're tapping into the existing Vite http server instead of creating a new one like I am. I tried doing this and I still get the same problem. Every time I try httpServer.listen(8080); on Vite's http server I'm told that the server was already listening on a port and the dev server terminates. I also tried manually creating an http server using require("http").createServer() and using that, but (unsurprisingly) that also did not work and acted identically to my initial attempt shown at the beginning of this question.
The tutorial seems to be booting up a Socket.io server the same way I'm trying to boot up mine, but they don't seem to be running into any conflicts like I am. I checked Socket.io's documentation to see if perhaps the Server constructor has a built-in failsafe to make sure it doesn't listen on a port if it's already listening on that port (and avoid creating the error), but the docs didn't give any information in that regard, so I'm still unsure as to what's going on there.
Is there any way to do what I'm trying to do, or am I going about this entirely the wrong way? I can't find hardly any information about this anywhere on the internet.
And also, what's going on in the Socket.io example that allows it to work where mine won't? Is Socket.io doing something special?
Here's my attempt at using Vite's built-in http server. This behaves the same as my other attempts.
vite.config.ts
import type { UserConfig } from 'vite';
import { sveltekit } from '#sveltejs/kit/vite';
import { WebSocketServer } from "ws";
const webSocketServer = {
name: "webSocketServer",
configureServer: (server: any) => {
const httpServer = server.httpServer;
const webSocketServer = new WebSocketServer({
noServer: true
});
webSocketServer.on("connection", (socket, request) => {
socket.on("message", (data, isBinary) => {
console.log(`Recieved ${data}`);
});
socket.send("hi c:");
});
httpServer.on("upgrade", (request: any, socket: any, head: any) => {
webSocketServer.handleUpgrade(request, socket, head, socket => {
webSocketServer.emit("connection", socket, request);
});
});
httpServer.listen(8080);
}
}
const config: UserConfig = {
plugins: [sveltekit(), webSocketServer]
};
export default config;

ws supports initialising with an existing server like socket.io
Don't store the application logic in the vite/svelte config, as it will be needed for the production build when vite is not available (I'm not familiar with sveltekit though so maybe it does some magic?). Create a standalone file for the ws setup:
import { WebSocketServer } from "ws";
export const configureServer = (server) => {
const webSocketServer = new WebSocketServer({
server: server.httpServer,
});
webSocketServer.on("connection", (socket, request) => {
socket.on("message", (data, isBinary) => {
console.log(`Recieved ${data}`);
});
socket.send("test from server");
});
}
export const webSocketServer = {
name: "webSocketServer",
configureServer,
}
Then you should be able to use your version of webSocketServer like the socket.io example as they both attach to the '#sveltejs/adapter-node' server.
import adapter from '#sveltejs/adapter-node'
import { webSocketServer } from './sockets.js';
/** #type {import('#sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
vite: {
plugins: [webSocketServer],
},
},
}
export default config
configureServer can now be reused when you setup the custom server for production.

Related

Create server available on HTTP/HTTPs that can be used with PM2

I'm trying to create a Socket.IO server that has the following goals:
Accessible on the local network of virtual machines using HTTP (http://<server-local-ip>)
That can be accessed via browser by users through the HTTPs protocol, and that can also make the socket.io.js bundle available via HTTPs (https://socket-server.example.com)
That uses all available CPUs in the virtual machine (the server will run in just one virtual machine) - (Possible with PM2)
Have the ability to be automatically restarted in case of failure (Possible with PM2)
For that I created a script based on the Socket.IO help article teaching how to use PM2 and this question that teaches to use HTTP and HTTPs.
/**
* pm2 start basic.js -i 0
*/
const http = require("http");
const https = require("https");
const { Server } = require("socket.io");
const { createAdapter } = require("#socket.io/cluster-adapter");
const { setupWorker } = require("#socket.io/sticky");
const { readFileSync } = require("fs");
const httpServer = http.createServer();
const httpsServer = https.createServer({
key: readFileSync("./localhost-key.pem"),
cert: readFileSync("./localhost.pem")
});
const io = new Server(httpServer, {
cors: {
origin: "*",
methods: ["GET", "POST"]
}
});
io.adapter(createAdapter());
setupWorker(io);
io.on("connection", (socket) => {
console.log(`connect ${socket.id}`);
});
httpsServer.on("request", (req, res) => {
io.engine.handleRequest(req, res);
});
httpsServer.on("upgrade", (req, socket, head) => {
io.engine.handleUpgrade(req, socket, head);
});
httpServer.listen(8080);
httpsServer.listen(4430);
Using HTTP and HTTPs always throws an error.
Via HTTPs I can't load the socket.io.js bundle. But as this service will be available via browser, it will be necessary to make it available via HTTPs to users.
Direct access via HTTPs displays:
{
code: 0,
message: "Transport unknown"
}
This is just using the first part of the script, without trying to run with PM2 yet.
When placing the PM2 part next to the script, other errors appear:
I have to remove the code httpServer.listen(3000); for HTTP to work
When I connect to HTTPs the code never finds the session, so it keeps trying to reconnect endlessly.
socket.io.js via HTTPs remains unreachable
Even using HTTP socket.io.js and connecting with <script src="http://localhost:8080/socket.io/socket.io.js"></script> <script> const socket = io('https://localhost:3001');</script> nothing works
However, if I run all this over HTTP only, without requiring HTTPs, it works perfectly.
What am I doing wrong for HTTP/HTTPs not to work together?
Will I have to make the server available only in HTTP and create a proxy via NGINX to support HTTPs and call the HTTP server?

How can I use express app in Supertest request when it gets ready asynchronously?

I have a nodejs application which starts asynchronously because of graphql.
require('custom-env').env();
import { DateTruncAggregateGroupSpecsPlugin } from './subgraphs/db/date_trunc_aggregate_group_specs_plugin';
import PgAggregatesPlugin from "#graphile/pg-aggregates";
import FederationPlugin from "#graphile/federation";
import ConnectionFilterPlugin from "postgraphile-plugin-connection-filter";
const PostGraphileDerivedFieldPlugin = require("postgraphile-plugin-derived-field");
import express from "express";
import { ApolloServer, gql } from "apollo-server-express";
const { makeSchemaAndPlugin } = require("postgraphile-apollo-server");
import pg from 'pg';
import { makeExtendSchemaPlugin } from "graphile-utils";
import { readFileSync } from 'fs';
import { resolve } from 'path';
import resolvers from './resolvers';
export let app = express();
export let server: any;
const { PORT, NODE_ENV, SCHEMA, DATABASE_URL } = process.env;
async function main() {
const { schema, plugin } = await makeSchemaAndPlugin(
new pg.Pool({
connectionString: DATABASE_URL
}),
SCHEMA,
{
subscriptions: false,
appendPlugins: [
FederationPlugin,
ConnectionFilterPlugin,
PostGraphileDerivedFieldPlugin,
PgAggregatesPlugin,
DateTruncAggregateGroupSpecsPlugin,
makeExtendSchemaPlugin((build) => ({
typeDefs: gql(readFileSync(resolve(__dirname, '../graphs/custom.graphql'), { encoding: 'utf-8' })),
resolvers
}))
],
graphileBuildOptions: {
connectionFilterRelations: true
}
}
);
const graphql = new ApolloServer({
debug: false,
schema,
plugins: [plugin],
introspection: true
});
await graphql.start();
graphql.applyMiddleware({
app,
path: '/graphql'
});
server = this.app.listen(PORT, () => console.info(`🚀 Running on PORT ${PORT} 🚀`));
}
main();
The above is my express server that adds graphql to it.
As you can see, the starting of the server is asynchronous.
Now I am using supertest to test APIs end-to-end. Supertest requires app to be passed in.
I need server to start before all tests in my project and tests to be able to use app for supertest reuqest.
How do I do that. With regualar server it is easy as starting of server is not asynchronous, so my app is ready to use by tests. But not in this case. How do I carry out supertest requests.

What address should I use for "localhost" in my Flutter app?

I was using the following code within my Flutter app and it was working with no problems, but today after I upgraded my Flutter, it doesn't work and gives me XMLHttpRequest error.
Future<void> _authenticate(
String email, String password, String urlSegment) async {
final host = UniversalPlatform.isAndroid ? '10.0.2.2' : '127.0.0.1';
final url = Uri.parse('http://$host:8000/api/$urlSegment');
try {
final http.Response response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: json.encode(
{
'email': email,
'password': password,
},
),
);
Does anything have been changed in new Flutter version? Should I change the following line of code that specifies my host address?
final host = UniversalPlatform.isAndroid ? '10.0.2.2' : '127.0.0.1';
EDIT: I tried to add cors to my NodeJS backend server and this is my app.ts file as following:
import express, { Request, Response, NextFunction } from "express";
import cors from "cors";
import dotenv from "dotenv";
dotenv.config();
import config from "config";
import responseTime from "response-time";
import connect from "./utils/connect";
import logger from "./utils/logger";
import routes from "./routes";
import deserializeUser from "./middleware/deserializeUser";
import { restResponseTimeHistogram, startMetricsServer } from "./utils/metrics";
import swaggerDocs from "./utils/swagger";
const allowedOrigins = ['http://localhost:8000' , 'https://10.0.2.2:8000', 'http://127.0.0.1:8000'];
const options: cors.CorsOptions = {
origin: allowedOrigins
};
const port = config.get<number>("port");
const app = express();
app.use(express.json());
app.use(cors(options));
app.use(deserializeUser);
app.use(
responseTime((req: Request, res: Response, time: number) => {
if (req?.route?.path) {
restResponseTimeHistogram.observe(
{
method: req.method,
route: req.route.path,
status_code: res.statusCode,
},
time * 1000
);
}
})
);
app.listen(port, async () => {
logger.info(`App is running at http://localhost:${port}`);
await connect();
routes(app);
startMetricsServer();
swaggerDocs(app, port);
});
But still doesn't work and I get the same error!
You've set the allowed origins to :8000, but that's the backend server's address. Instead, you need to set it to the Flutter debug server's address (and eventually to the web server where you host the production app, if that's not exactly the same as the backend server). (You can remove all the addresses ending in 8000.)
The problem is that the debug server picks a random port for each run. You can tell it to use a fixed port and then that becomes the port you need to include in your allowed origins.
Add --web-hostname=localhost --web-port=9999 as command line parameters to where you run your main.dart, then add localhost:9999 as an allowed origin.
(As a get-out-of-jail, also try * as an allowed origin.)
Finally, you should probably explicitly set the CORS allowed methods to the list of methods your server's API expects; probably OPTIONS, GET and POST.

Multiple services using pm2 with Websockets on Azure Linux Web App, which ports?

I'm working on creating a WebApp using React and Websockets building it of from this example here: https://learn.microsoft.com/en-us/azure/iot-hub/iot-hub-live-data-visualization-in-web-apps
Locally everything is working fine. I have the React frontend code in a separate folder as well as the websocketserver code.
I'm trying to both into a single Azure Linux Web App (Basic tier not free) and starting it up with pm2 and this ecosystem.config.js:
module.exports = {
apps : [
{
name : "frontend",
script : "serve",
env: {
PM2_SERVE_PATH: './frontend/build',
PM2_SERVE_SPA: 'true',
}
},
{
name : "websocketserver",
script : "./websocketserver/server.js",
}
]
}
Both services are starting up correctly but the websocket connection is unable to connect.
The server in server.js looks like this:
const server = http.createServer();
const wss = new WebSocket.Server({ server });
wss.broadcast = (data) => {
if (wss.clients.size === 0) {
console.log(`No clients connected. Broadcasting over port ${server.address().port}`);
// console.log(`No clients connected. Broadcasting over port ${wss.options.port}`);
} else {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
try {
console.log(`Broadcasting data ${data}`);
client.send(data);
} catch (e) {
console.error(e);
}
}
})
}
};
server.listen('8081', () => {
console.log('Listening on %d.', server.address().port);
});
If I use process.env.PORT || '3000' in server.listen(), then I get an EAINUSE error as it conflicts with the frontend 8080 port, so I handpicked 8081.
I have not found any useful article about this. Can anybody help and advice what to try?
Thanks!
I have followed the given documentation and able to get the desired result accordingly after changing the PORT to 8081.
Initially with PORT 3000, I got the below error.
I have changed the PORT to 8081 in server.js.
After changing the PORT you need to set IotHubConnectionString and EventHubConsumerGroup once again and run
npm install and npm start
Please follow the document correctly and check once again.
Set the PORT environment variable respective value in your App Service config, ref https://learn.microsoft.com/en-us/azure/app-service/configure-language-nodejs

Can't get redis cache to work with apollo server express

I'm trying to setup a caching system for my apollo express server, but it seems like the guides I've followed from the official docs: https://www.apollographql.com/docs/apollo-server/performance/caching/#memcachedredis-setup do not appear to work for me.
I can verify that in my resolver for a query that it keeps getting hit by console logging that it is being hit each time instead of only being hit once and then the cache being set
// my resolver definition
const getUserProfile = async (_, args, context, info) => {
info.cacheControl.setCacheHint({ maxAge: 300 });
console.log('MISS, set cache hint') // this console logs every time instead of just once
return context;
};
Here is my code:
server.js
...
const redisClient = require('./cacheClient');
let httpServer;
if (process.env.ENV === 'prod') {
httpServer = httpCreateServer(app);
} else {
httpServer = httpsCreateServer(
{
key: fs.readFileSync('./localhost-key.pem'),
cert: fs.readFileSync('./localhost.pem'),
},
app
);
}
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
cache: new BaseRedisCache({
client: redisClient
}),
cacheControl: true,
});
apolloServer.context = ({ req }) => ({
...
});
await apolloServer.start();
apolloServer.applyMiddleware({ app });
httpServer.listen(PORT, () =>
console.log(`Server is now running on port ${PORT}`)
);
cacheClient.js
require('dotenv').config();
const { REDIS_SERVER, REDIS_PORT } = process.env;
const redis = require('redis');
const client = redis.createClient({
host: REDIS_SERVER,
port: REDIS_PORT,
});
client.on('connect', () => {
console.log('Redis client connected');
});
client.on('message', (channel, message) => {
console.log(`Received ${message} from ${channel}`);
});
module.exports = client;
I'm also making sure my web app is sending a Cache-Control: max-age=300 header on the graphql query as well. I also checked my Redis server and verified that no new keys are added after making the graphql query.
I have also tried setting the cacheControl directive onto the type like:
type UserProfile #cacheControl(maxAge: 300) {
...
}
but no luck with that either. I appreciate any help, thank you!
Whether you use koa or express, you will need to add these plugins: https://www.apollographql.com/docs/apollo-server/api/plugin/cache-control/
https://www.apollographql.com/docs/apollo-server/performance/caching#identifying-users-for-private-responses
...
const responseCachePlugin = require('apollo-server-plugin-response-cache').default;
const { ApolloServerPluginCacheControl } = require("apollo-server-core");
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [responseCachePlugin({
sessionId: ({ context }) => (context.ctx.header ? context.ctx.header.authorization : null),
}),
ApolloServerPluginCacheControl({
// Cache everything for 1000 seconds.
defaultMaxAge: 1000,
// Don't send the `cache-control` response header.
calculateHttpHeaders: false,
}),
],
});
Important Notes:
Change this code context.ctx.header ? context.ctx.header.authorization to get session Id for specific user.
Make sure that the version of apollo-server-express you use will fire events willResolveField for getting values from the cache and willSendResponse for setting the value in cache when needed (because I had a problem with v2.25.x in Koa).
Using const redis = require('redis'); will not set ttl (maxAge) correctly. So, please make sure to use ioredis instead to be able to set ttl.

Resources