Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
6 changes: 6 additions & 0 deletions .changeset/moody-cheetahs-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@callstack/repack-dev-server": minor
"@callstack/repack": minor
---

Reworked DevServer HMR pipeline - improved performance & recovery from errors
4 changes: 2 additions & 2 deletions apps/tester-app/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PODS:
- boost (1.84.0)
- callstack-repack (5.0.0-rc.10):
- callstack-repack (5.0.0-rc.11):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -2011,7 +2011,7 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
callstack-repack: 3ffe9c49dd09333ddb3cd0ac85ff6724dd6fa560
callstack-repack: 028c13131834d77421120d3dc1de340f10899bcb
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
FBLazyVector: be7314029d6ec6b90f0f75ce1195b8130ed9ac4f
fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be
Expand Down
4 changes: 2 additions & 2 deletions apps/tester-federation-v2/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PODS:
- boost (1.84.0)
- callstack-repack (5.0.0-rc.10):
- callstack-repack (5.0.0-rc.11):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -1895,7 +1895,7 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
callstack-repack: 3ffe9c49dd09333ddb3cd0ac85ff6724dd6fa560
callstack-repack: 028c13131834d77421120d3dc1de340f10899bcb
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
FBLazyVector: be7314029d6ec6b90f0f75ce1195b8130ed9ac4f
fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be
Expand Down
4 changes: 2 additions & 2 deletions apps/tester-federation/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PODS:
- boost (1.84.0)
- callstack-repack (5.0.0-rc.10):
- callstack-repack (5.0.0-rc.11):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -1919,7 +1919,7 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
callstack-repack: 3ffe9c49dd09333ddb3cd0ac85ff6724dd6fa560
callstack-repack: 028c13131834d77421120d3dc1de340f10899bcb
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
FBLazyVector: be7314029d6ec6b90f0f75ce1195b8130ed9ac4f
fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be
Expand Down
4 changes: 2 additions & 2 deletions packages/dev-server/src/createServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ export async function createServer(config: Server.Config) {
platform,
});
},
broadcastToHmrClients: (event, platform, clientIds) => {
instance.wss.hmrServer.send(event, platform, clientIds);
broadcastToHmrClients: (event) => {
instance.wss.hmrServer.send(event);
},
broadcastToMessageClients: ({ method, params }) => {
instance.wss.messageServer.broadcast(method, params);
Expand Down
7 changes: 4 additions & 3 deletions packages/dev-server/src/plugins/compiler/compilerPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ async function compilerPlugin(

if (!filename) {
// This technically should never happen - this route should not be called if file is missing.
request.log.error('File was not provided');
return reply.notFound();
request.log.debug('File was not provided');
return reply.notFound('File was not provided');
}

// Let consumer infer the platform. If function is not provided fallback
Expand All @@ -42,6 +42,7 @@ async function compilerPlugin(
JSON.stringify({
done: completed,
total,
status: 'Bundling with Re.Pack',
})
);
};
Expand Down Expand Up @@ -70,7 +71,7 @@ async function compilerPlugin(
return reply.code(200).type(mimeType).send(asset);
}
} catch (error) {
request.log.error(error);
request.log.debug(error);
return reply.notFound((error as Error).message);
}
},
Expand Down
1 change: 0 additions & 1 deletion packages/dev-server/src/plugins/favicon/faviconPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { FastifyInstance } from 'fastify';
import fastifyFavicon from 'fastify-favicon';
import fastifyPlugin from 'fastify-plugin';

// @ts-ignore
const dirname = path.dirname(fileURLToPath(import.meta.url));
const pathToImgDir = path.join(dirname, '../../../static');

Expand Down
84 changes: 59 additions & 25 deletions packages/dev-server/src/plugins/wss/WebSocketServer.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,63 @@
import type { IncomingMessage } from 'node:http';
import type { Socket } from 'node:net';
import type { FastifyInstance } from 'fastify';
import {
type ServerOptions,
type WebSocket,
WebSocketServer as WebSocketServerImpl,
} from 'ws';
import type { WebSocketServerInterface } from './types.js';
import { type WebSocket, WebSocketServer as WebSocketServerImpl } from 'ws';
import type {
WebSocketServerInterface,
WebSocketServerOptions,
} from './types.js';

/**
* Abstract class for providing common logic (eg routing) for all WebSocket servers.
*
* @category Development server
*/
export abstract class WebSocketServer implements WebSocketServerInterface {
export abstract class WebSocketServer<T extends WebSocket = WebSocket>
implements WebSocketServerInterface
{
/** An instance of the underlying WebSocket server. */
protected server: WebSocketServerImpl;

/** Fastify instance from which {@link server} will receive upgrade connections. */
protected fastify: FastifyInstance;

protected name: string;

protected paths: string[];

protected clients: Map<string, T>;
protected nextClientId = 0;

private timer: NodeJS.Timer | null = null;

/**
* Create a new instance of the WebSocketServer.
* Any logging information, will be passed through standard `fastify.log` API.
*
* @param fastify Fastify instance to which the WebSocket will be attached to.
* @param path Path on which this WebSocketServer will be accepting connections.
* @param wssOptions WebSocket Server options.
* @param options WebSocketServer options.
*/
constructor(
fastify: FastifyInstance,
path: string | string[],
wssOptions: Omit<
ServerOptions,
'noServer' | 'server' | 'host' | 'port' | 'path'
> = {}
) {
constructor(fastify: FastifyInstance, options: WebSocketServerOptions) {
this.fastify = fastify;
this.server = new WebSocketServerImpl({ noServer: true, ...wssOptions });
this.name = options.name;
this.paths = Array.isArray(options.path) ? options.path : [options.path];
this.server = new WebSocketServerImpl({ noServer: true, ...options.wss });
this.server.on('connection', this.onConnection.bind(this));
this.paths = Array.isArray(path) ? path : [path];

this.clients = new Map();

// setup heartbeat timer
this.timer = setInterval(() => {
this.clients.forEach((socket) => {
if (!socket.isAlive) {
socket.terminate();
} else {
socket.isAlive = false;
socket.ping(() => {});
}
});
}, 30000);
}

shouldUpgrade(pathname: string) {
Expand All @@ -54,11 +70,29 @@ export abstract class WebSocketServer implements WebSocketServerInterface {
});
}

/**
* Process incoming WebSocket connection.
*
* @param socket Incoming WebSocket connection.
* @param request Upgrade request for the connection.
*/
abstract onConnection(socket: WebSocket, request: IncomingMessage): void;
onConnection(socket: T, _request: IncomingMessage): string {
const clientId = `client#${this.nextClientId++}`;
this.clients.set(clientId, socket);
this.fastify.log.debug({ msg: `${this.name} client connected`, clientId });

const errorHandler = () => {
this.fastify.log.debug({
msg: `${this.name} client disconnected`,
clientId,
});
socket.removeAllListeners(); // should we do this?
this.clients.delete(clientId);
};

socket.addEventListener('error', errorHandler);
socket.addEventListener('close', errorHandler);

// heartbeat
socket.isAlive = true;
socket.on('pong', () => {
socket.isAlive = true;
});

return clientId;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { FastifyInstance } from 'fastify';
import type WebSocket from 'ws';
import { WebSocketServer } from '../WebSocketServer.js';

/**
Expand All @@ -9,17 +8,14 @@ import { WebSocketServer } from '../WebSocketServer.js';
* @category Development server
*/
export class WebSocketApiServer extends WebSocketServer {
private clients = new Map<string, WebSocket>();
private nextClientId = 0;

/**
* Create new instance of WebSocketApiServer and attach it to the given Fastify instance.
* Any logging information, will be passed through standard `fastify.log` API.
*
* @param fastify Fastify instance to attach the WebSocket server to.
*/
constructor(fastify: FastifyInstance) {
super(fastify, '/api');
super(fastify, { name: 'API', path: '/api' });
}

/**
Expand All @@ -38,28 +34,4 @@ export class WebSocketApiServer extends WebSocketServer {
}
}
}

/**
* Process new WebSocket connection from client application.
*
* @param socket Incoming client's WebSocket connection.
*/
onConnection(socket: WebSocket) {
const clientId = `client#${this.nextClientId++}`;
this.clients.set(clientId, socket);

this.fastify.log.debug({ msg: 'API client connected', clientId });
this.clients.set(clientId, socket);

const onClose = () => {
this.fastify.log.debug({
msg: 'API client disconnected',
clientId,
});
this.clients.delete(clientId);
};

socket.addEventListener('error', onClose);
socket.addEventListener('close', onClose);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { IncomingMessage } from 'node:http';
import type { FastifyInstance } from 'fastify';
import type WebSocket from 'ws';
import { WebSocketServer } from '../WebSocketServer.js';
Expand All @@ -9,17 +10,14 @@ import { WebSocketServer } from '../WebSocketServer.js';
* @category Development server
*/
export class WebSocketDevClientServer extends WebSocketServer {
private clients = new Map<string, WebSocket>();
private nextClientId = 0;

/**
* Create new instance of WebSocketDevClientServer and attach it to the given Fastify instance.
* Any logging information, will be passed through standard `fastify.log` API.
*
* @param fastify Fastify instance to attach the WebSocket server to.
*/
constructor(fastify: FastifyInstance) {
super(fastify, '/__client');
super(fastify, { name: 'React Native', path: '/__client' });
}

/**
Expand Down Expand Up @@ -47,28 +45,13 @@ export class WebSocketDevClientServer extends WebSocketServer {
}
}

/**
* Process new WebSocket connection from client application.
*
* @param socket Incoming client's WebSocket connection.
*/
onConnection(socket: WebSocket) {
const clientId = `client#${this.nextClientId++}`;
this.clients.set(clientId, socket);
this.fastify.log.debug({ msg: 'React Native client connected', clientId });

const onClose = () => {
this.fastify.log.debug({
msg: 'React Native client disconnected',
clientId,
});
this.clients.delete(clientId);
};
override onConnection(socket: WebSocket, request: IncomingMessage): string {
const clientId = super.onConnection(socket, request);

socket.addEventListener('error', onClose);
socket.addEventListener('close', onClose);
socket.addEventListener('message', (event) => {
this.processMessage(event.data.toString());
});

return clientId;
}
}
Loading