hightjs 0.2.41 → 0.2.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hotReload.js CHANGED
@@ -43,20 +43,29 @@ class HotReloadManager {
43
43
  constructor(projectDir) {
44
44
  this.wss = null;
45
45
  this.watchers = [];
46
- this.clients = new Set();
47
- this.pingInterval = null;
46
+ this.clients = new Map();
48
47
  this.backendApiChangeCallback = null;
49
48
  this.frontendChangeCallback = null;
49
+ this.isShuttingDown = false;
50
+ this.debounceTimers = new Map();
51
+ this.customHotReloadListener = null;
50
52
  this.projectDir = projectDir;
51
53
  }
52
54
  async start() {
53
- // Não cria servidor na porta separada - será integrado ao Express
54
55
  this.setupWatchers();
55
56
  }
56
- // Novo método para integrar com Express
57
+ // Método para integrar com Express
57
58
  handleUpgrade(request, socket, head) {
59
+ if (this.isShuttingDown) {
60
+ socket.destroy();
61
+ return;
62
+ }
58
63
  if (!this.wss) {
59
- this.wss = new ws_1.WebSocketServer({ noServer: true });
64
+ this.wss = new ws_1.WebSocketServer({
65
+ noServer: true,
66
+ perMessageDeflate: false, // Desabilita compressão para melhor performance
67
+ maxPayload: 1024 * 1024 // Limite de 1MB por mensagem
68
+ });
60
69
  this.setupWebSocketServer();
61
70
  }
62
71
  this.wss.handleUpgrade(request, socket, head, (ws) => {
@@ -67,179 +76,317 @@ class HotReloadManager {
67
76
  if (!this.wss)
68
77
  return;
69
78
  this.wss.on('connection', (ws) => {
70
- this.clients.add(ws);
71
- // Setup ping/pong para manter conexão viva
72
- const ping = () => {
73
- if (ws.readyState === ws_1.WebSocket.OPEN) {
79
+ if (this.isShuttingDown) {
80
+ ws.close();
81
+ return;
82
+ }
83
+ // Setup ping/pong para detectar conexões mortas
84
+ const pingTimer = setInterval(() => {
85
+ const client = this.clients.get(ws);
86
+ if (client && ws.readyState === ws_1.WebSocket.OPEN) {
87
+ // Se não recebeu pong há mais de 60 segundos, desconecta
88
+ if (Date.now() - client.lastPong > 60000) {
89
+ ws.terminate();
90
+ return;
91
+ }
74
92
  ws.ping();
75
93
  }
94
+ }, 30000);
95
+ const clientConnection = {
96
+ ws,
97
+ pingTimer,
98
+ lastPong: Date.now()
76
99
  };
77
- const pingTimer = setInterval(ping, 30000); // Ping a cada 30 segundos
100
+ this.clients.set(ws, clientConnection);
78
101
  ws.on('pong', () => {
79
- // Cliente respondeu ao ping - conexão ainda ativa
102
+ const client = this.clients.get(ws);
103
+ if (client) {
104
+ client.lastPong = Date.now();
105
+ }
80
106
  });
81
107
  ws.on('close', () => {
82
- this.clients.delete(ws);
83
- clearInterval(pingTimer);
108
+ this.cleanupClient(ws);
84
109
  });
85
- ws.on('error', () => {
86
- this.clients.delete(ws);
87
- clearInterval(pingTimer);
110
+ ws.on('error', (error) => {
111
+ console_1.default.logWithout(console_1.Levels.ERROR, `WebSocket error: ${error.message}`);
112
+ this.cleanupClient(ws);
88
113
  });
114
+ console_1.default.logWithout(console_1.Levels.INFO, '🔌 Hot-reload cliente conectado');
89
115
  });
90
116
  }
117
+ cleanupClient(ws) {
118
+ const client = this.clients.get(ws);
119
+ if (client) {
120
+ clearInterval(client.pingTimer);
121
+ this.clients.delete(ws);
122
+ }
123
+ }
91
124
  setupWatchers() {
92
- // 1. Watcher para arquivos frontend (rotas, componentes) - EXCLUINDO backend
93
- const frontendWatcher = chokidar.watch([
94
- path.join(this.projectDir, 'src/web/**/*.{tsx,ts,jsx,js}'),
125
+ // Remove watchers antigos e use apenas um watcher global para src
126
+ const debouncedChange = this.debounce((filePath) => {
127
+ this.handleAnySrcChange(filePath);
128
+ }, 100);
129
+ const watcher = chokidar.watch([
130
+ path.join(this.projectDir, 'src/**/*'),
95
131
  ], {
96
132
  ignored: [
97
133
  /(^|[\/\\])\../, // arquivos ocultos
98
- path.join(this.projectDir, 'src/web/backend/**/*') // exclui toda a pasta backend
134
+ '**/node_modules/**',
135
+ '**/.git/**',
136
+ '**/dist/**'
99
137
  ],
100
138
  persistent: true,
101
- ignoreInitial: true
139
+ ignoreInitial: true,
140
+ usePolling: false,
141
+ awaitWriteFinish: {
142
+ stabilityThreshold: 100,
143
+ pollInterval: 50
144
+ }
102
145
  });
103
- frontendWatcher.on('change', async (filePath) => {
104
- console_1.default.logWithout(console_1.Levels.INFO, `🔄 Frontend alterado: ${filePath}`);
146
+ watcher.on('change', debouncedChange);
147
+ watcher.on('add', debouncedChange);
148
+ watcher.on('unlink', (filePath) => {
149
+ console_1.default.info(`🗑️ Arquivo removido: ${path.basename(filePath)}`);
105
150
  (0, router_1.clearFileCache)(filePath);
106
- // Checa build do arquivo alterado
107
- const result = await this.checkFrontendBuild(filePath);
108
- if (result.error) {
109
- this.notifyClients('frontend-error', { file: filePath, error: result.error });
110
- }
111
- else {
112
- this.frontendChangeCallback?.();
113
- this.notifyClients('frontend-reload');
114
- }
151
+ this.clearBackendCache(filePath);
152
+ this.frontendChangeCallback?.();
153
+ this.backendApiChangeCallback?.();
154
+ this.notifyClients('src-reload', { file: filePath, event: 'unlink' });
115
155
  });
116
- frontendWatcher.on('add', async (filePath) => {
117
- console_1.default.info(`➕ Novo arquivo frontend: ${path.basename(filePath)}`);
156
+ this.watchers.push(watcher);
157
+ }
158
+ debounce(func, wait) {
159
+ return (...args) => {
160
+ const key = args[0]; // usa o primeiro argumento como chave
161
+ const existingTimer = this.debounceTimers.get(key);
162
+ if (existingTimer) {
163
+ clearTimeout(existingTimer);
164
+ }
165
+ const timer = setTimeout(() => {
166
+ this.debounceTimers.delete(key);
167
+ func.apply(this, args);
168
+ }, wait);
169
+ this.debounceTimers.set(key, timer);
170
+ };
171
+ }
172
+ async handleAnySrcChange(filePath) {
173
+ console_1.default.logWithout(console_1.Levels.INFO, `🔄 Arquivo alterado: ${path.basename(filePath)}`);
174
+ // Detecta se é arquivo de frontend ou backend
175
+ const isFrontendFile = filePath.includes(path.join('src', 'web', 'routes')) ||
176
+ filePath.includes(path.join('src', 'web', 'components')) ||
177
+ filePath.includes('layout.tsx') ||
178
+ filePath.includes('not-found.tsx') ||
179
+ filePath.endsWith('.tsx') ||
180
+ filePath.endsWith('.jsx');
181
+ const isBackendFile = filePath.includes(path.join('src', 'web', 'backend')) ||
182
+ (filePath.includes(path.join('src', 'web')) && !isFrontendFile);
183
+ // Limpa o cache do arquivo alterado
184
+ (0, router_1.clearFileCache)(filePath);
185
+ this.clearBackendCache(filePath);
186
+ // Checa build se for .ts/.tsx/.js/.jsx
187
+ const ext = path.extname(filePath);
188
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
118
189
  const result = await this.checkFrontendBuild(filePath);
119
190
  if (result.error) {
120
- this.notifyClients('frontend-error', { file: filePath, error: result.error });
191
+ this.notifyClients('src-error', { file: filePath, error: result.error });
192
+ return;
121
193
  }
122
- else {
123
- this.frontendChangeCallback?.();
124
- this.notifyClients('frontend-reload');
125
- }
126
- });
127
- frontendWatcher.on('unlink', (filePath) => {
128
- console_1.default.info(`🗑️ Arquivo frontend removido: ${path.basename(filePath)}`);
129
- (0, router_1.clearFileCache)(filePath);
194
+ }
195
+ // Se for arquivo de frontend, notifica o cliente para recarregar a página
196
+ if (isFrontendFile) {
197
+ console_1.default.logWithout(console_1.Levels.INFO, `📄 Recarregando frontend...`);
130
198
  this.frontendChangeCallback?.();
131
- this.notifyClients('frontend-reload');
132
- });
133
- // 2. Watcher específico para rotas de API backend
134
- const backendApiWatcher = chokidar.watch([
135
- path.join(this.projectDir, 'src/web/backend/routes/**/*.{ts,tsx,js,jsx}'),
136
- ], {
137
- ignored: /(^|[\/\\])\../,
138
- persistent: true,
139
- ignoreInitial: true
140
- });
141
- backendApiWatcher.on('change', (filePath) => {
142
- console_1.default.info(`🔄 API backend alterada: ${path.basename(filePath)}`);
143
- this.clearBackendCache(filePath);
144
- this.notifyClients('backend-api-reload');
145
- // Chama o callback, se definido
199
+ this.notifyClients('frontend-reload', { file: filePath, event: 'change' });
200
+ }
201
+ // Se for arquivo de backend, recarrega o módulo e notifica
202
+ if (isBackendFile) {
203
+ console_1.default.logWithout(console_1.Levels.INFO, `⚙️ Recarregando backend...`);
146
204
  this.backendApiChangeCallback?.();
147
- });
148
- backendApiWatcher.on('add', (filePath) => {
149
- console_1.default.info(`➕ Nova API backend: ${path.basename(filePath)}`);
150
- this.notifyClients('backend-api-reload');
151
- });
152
- backendApiWatcher.on('unlink', (filePath) => {
153
- console_1.default.info(`🗑️ API backend removida: ${path.basename(filePath)}`);
154
- this.clearBackendCache(filePath);
155
- this.notifyClients('backend-api-reload');
156
- });
157
- // 3. Watcher para arquivos backend (server.ts, configs)
158
- const backendWatcher = chokidar.watch([
159
- path.join(this.projectDir, 'src/server.ts'),
160
- path.join(this.projectDir, 'src/**/*.ts'),
161
- '!**/src/web/**', // exclui pasta web
162
- ], {
163
- ignored: /(^|[\/\\])\../,
164
- persistent: true,
165
- ignoreInitial: true
166
- });
167
- backendWatcher.on('change', () => {
168
- this.restartServer();
169
- });
170
- this.watchers.push(frontendWatcher, backendApiWatcher, backendWatcher);
205
+ this.notifyClients('backend-api-reload', { file: filePath, event: 'change' });
206
+ }
207
+ // Fallback: se não for nem frontend nem backend detectado, recarrega tudo
208
+ if (!isFrontendFile && !isBackendFile) {
209
+ console_1.default.logWithout(console_1.Levels.INFO, `🔄 Recarregando aplicação...`);
210
+ this.frontendChangeCallback?.();
211
+ this.backendApiChangeCallback?.();
212
+ this.notifyClients('src-reload', { file: filePath, event: 'change' });
213
+ }
214
+ // Chama listener customizado se definido
215
+ if (this.customHotReloadListener) {
216
+ try {
217
+ await this.customHotReloadListener(filePath);
218
+ }
219
+ catch (error) {
220
+ // @ts-ignore
221
+ console_1.default.logWithout(console_1.Levels.ERROR, `Erro no listener customizado: ${error.message}`);
222
+ }
223
+ }
171
224
  }
172
225
  notifyClients(type, data) {
226
+ if (this.isShuttingDown || this.clients.size === 0) {
227
+ return;
228
+ }
173
229
  const message = JSON.stringify({ type, data, timestamp: Date.now() });
174
- this.clients.forEach((client) => {
175
- if (client.readyState === ws_1.WebSocket.OPEN) {
176
- client.send(message);
230
+ const deadClients = [];
231
+ this.clients.forEach((client, ws) => {
232
+ if (ws.readyState === ws_1.WebSocket.OPEN) {
233
+ try {
234
+ ws.send(message);
235
+ }
236
+ catch (error) {
237
+ console_1.default.logWithout(console_1.Levels.ERROR, `Erro ao enviar mensagem WebSocket: ${error}`);
238
+ deadClients.push(ws);
239
+ }
240
+ }
241
+ else {
242
+ deadClients.push(ws);
177
243
  }
178
244
  });
245
+ // Remove clientes mortos
246
+ deadClients.forEach(ws => this.cleanupClient(ws));
179
247
  }
180
248
  restartServer() {
181
- // Notifica clientes que o servidor está reiniciando
182
249
  this.notifyClients('server-restart');
183
- // Aguarda um pouco e tenta reconectar
184
250
  setTimeout(() => {
185
251
  this.notifyClients('server-ready');
186
252
  }, 2000);
187
253
  }
188
254
  stop() {
255
+ this.isShuttingDown = true;
256
+ // Limpa todos os debounce timers
257
+ this.debounceTimers.forEach(timer => clearTimeout(timer));
258
+ this.debounceTimers.clear();
189
259
  // Para todos os watchers
190
260
  this.watchers.forEach(watcher => watcher.close());
191
261
  this.watchers = [];
192
- // Para ping interval
193
- if (this.pingInterval) {
194
- clearInterval(this.pingInterval);
195
- }
262
+ // Limpa todos os clientes
263
+ this.clients.forEach((client, ws) => {
264
+ clearInterval(client.pingTimer);
265
+ if (ws.readyState === ws_1.WebSocket.OPEN) {
266
+ ws.close();
267
+ }
268
+ });
269
+ this.clients.clear();
196
270
  // Fecha WebSocket server
197
271
  if (this.wss) {
198
272
  this.wss.close();
273
+ this.wss = null;
199
274
  }
200
275
  }
201
- // Retorna o script do cliente para injetar no HTML
276
+ // Script do cliente otimizado com reconnection backoff
202
277
  getClientScript() {
203
278
  return `
204
279
  <script>
205
280
  (function() {
206
281
  if (typeof window !== 'undefined') {
207
282
  let ws;
208
- let reconnectInterval;
283
+ let reconnectAttempts = 0;
284
+ let maxReconnectInterval = 30000; // 30 segundos max
285
+ let reconnectInterval = 1000; // Começa com 1 segundo
286
+ let reconnectTimer;
287
+ let isConnected = false;
209
288
 
210
289
  function connect() {
211
- ws = new WebSocket('ws://localhost:3000/hweb-hotreload/');
290
+ // Evita múltiplas tentativas simultâneas
291
+ if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
292
+ return;
293
+ }
294
+
295
+ try {
296
+ ws = new WebSocket('ws://localhost:3000/hweb-hotreload/');
297
+
298
+ ws.onopen = function() {
299
+ console.log('🔌 Hot-reload conectado');
300
+ isConnected = true;
301
+ reconnectAttempts = 0;
302
+ reconnectInterval = 1000;
303
+ clearTimeout(reconnectTimer);
304
+ };
305
+
306
+ ws.onmessage = function(event) {
307
+ try {
308
+ const message = JSON.parse(event.data);
309
+
310
+ switch(message.type) {
311
+ case 'frontend-reload':
312
+ window.location.reload();
313
+ break;
314
+ case 'backend-api-reload':
315
+ // Recarrega apenas se necessário
316
+ window.location.reload();
317
+ break;
318
+ case 'server-restart':
319
+ console.log('🔄 Servidor reiniciando...');
320
+ break;
321
+ case 'server-ready':
322
+ setTimeout(() => window.location.reload(), 500);
323
+ break;
324
+ case 'frontend-error':
325
+ console.error('❌ Erro no frontend:', message.data);
326
+ break;
327
+ }
328
+ } catch (e) {
329
+ console.error('Erro ao processar mensagem do hot-reload:', e);
330
+ }
331
+ };
332
+
333
+ ws.onclose = function(event) {
334
+ isConnected = false;
335
+
336
+ // Não tenta reconectar se foi fechamento intencional
337
+ if (event.code === 1000) {
338
+ return;
339
+ }
340
+
341
+ scheduleReconnect();
342
+ };
343
+
344
+ ws.onerror = function(error) {
345
+ isConnected = false;
346
+ // Não loga erros de conexão para evitar spam no console
347
+ };
348
+
349
+ } catch (error) {
350
+ console.error('Erro ao criar WebSocket:', error);
351
+ scheduleReconnect();
352
+ }
353
+ }
354
+
355
+ function scheduleReconnect() {
356
+ if (reconnectTimer) {
357
+ clearTimeout(reconnectTimer);
358
+ }
212
359
 
213
- ws.onopen = function() {
214
- clearInterval(reconnectInterval);
215
- };
360
+ reconnectAttempts++;
216
361
 
217
- ws.onmessage = function(event) {
218
- const message = JSON.parse(event.data);
219
-
220
- switch(message.type) {
221
- case 'frontend-reload':
222
- window.location.reload();
223
- break;
224
- case 'server-restart':
225
- break;
226
- case 'server-ready':
227
- setTimeout(() => window.location.reload(), 500);
228
- break;
229
- }
230
- };
362
+ // Exponential backoff com jitter
363
+ const baseInterval = Math.min(reconnectInterval * Math.pow(1.5, reconnectAttempts - 1), maxReconnectInterval);
364
+ const jitter = Math.random() * 1000; // Adiciona até 1 segundo de variação
365
+ const finalInterval = baseInterval + jitter;
231
366
 
232
- ws.onclose = function() {
233
- reconnectInterval = setInterval(() => {
367
+ reconnectTimer = setTimeout(() => {
368
+ if (!isConnected) {
234
369
  connect();
235
- }, 1000);
236
- };
237
-
238
- ws.onerror = function() {
239
- // Silencioso - sem logs
240
- };
370
+ }
371
+ }, finalInterval);
241
372
  }
242
373
 
374
+ // Detecta quando a página está sendo fechada para evitar reconexões desnecessárias
375
+ window.addEventListener('beforeunload', function() {
376
+ if (ws && ws.readyState === WebSocket.OPEN) {
377
+ ws.close(1000, 'Page unloading');
378
+ }
379
+ clearTimeout(reconnectTimer);
380
+ });
381
+
382
+ // Detecta quando a aba fica visível novamente para reconectar se necessário
383
+ document.addEventListener('visibilitychange', function() {
384
+ if (!document.hidden && !isConnected) {
385
+ reconnectAttempts = 0; // Reset do contador quando a aba fica ativa
386
+ connect();
387
+ }
388
+ });
389
+
243
390
  connect();
244
391
  }
245
392
  })();
@@ -247,46 +394,59 @@ class HotReloadManager {
247
394
  `;
248
395
  }
249
396
  clearBackendCache(filePath) {
250
- // Limpa o cache do require para forçar reload da rota de API
251
397
  const absolutePath = path.resolve(filePath);
252
398
  delete require.cache[absolutePath];
253
- // Também limpa dependências relacionadas
399
+ // Limpa dependências relacionadas de forma mais eficiente
400
+ const dirname = path.dirname(absolutePath);
254
401
  Object.keys(require.cache).forEach(key => {
255
- if (key.includes(path.dirname(absolutePath))) {
402
+ if (key.startsWith(dirname)) {
256
403
  delete require.cache[key];
257
404
  }
258
405
  });
259
406
  }
260
- // Método para registrar callback de mudança de API backend
261
407
  onBackendApiChange(callback) {
262
408
  this.backendApiChangeCallback = callback;
263
409
  }
264
- // Método para registrar callback de mudança de frontend
265
410
  onFrontendChange(callback) {
266
411
  this.frontendChangeCallback = callback;
267
412
  }
413
+ setHotReloadListener(listener) {
414
+ this.customHotReloadListener = listener;
415
+ console_1.default.info('🔌 Hot reload listener customizado registrado');
416
+ }
417
+ removeHotReloadListener() {
418
+ this.customHotReloadListener = null;
419
+ }
268
420
  async checkFrontendBuild(filePath) {
269
- // Usa ts-node para checar erros de compilação do arquivo alterado
270
- const tsNodePath = require.resolve('ts-node');
271
- const { spawn } = require('child_process');
272
- return new Promise((resolve) => {
273
- const proc = spawn(process.execPath, [tsNodePath, '--transpile-only', filePath], {
274
- cwd: this.projectDir,
275
- env: process.env,
276
- });
277
- let errorMsg = '';
278
- proc.stderr.on('data', (data) => {
279
- errorMsg += data.toString();
421
+ try {
422
+ const tsNodePath = require.resolve('ts-node');
423
+ const { spawn } = require('child_process');
424
+ return new Promise((resolve) => {
425
+ const proc = spawn(process.execPath, [tsNodePath, '--transpile-only', filePath], {
426
+ cwd: this.projectDir,
427
+ env: process.env,
428
+ timeout: 10000 // Timeout de 10 segundos
429
+ });
430
+ let errorMsg = '';
431
+ proc.stderr.on('data', (data) => {
432
+ errorMsg += data.toString();
433
+ });
434
+ proc.on('close', (code) => {
435
+ if (code !== 0 && errorMsg) {
436
+ resolve({ error: errorMsg });
437
+ }
438
+ else {
439
+ resolve({});
440
+ }
441
+ });
442
+ proc.on('error', (error) => {
443
+ resolve({ error: error.message });
444
+ });
280
445
  });
281
- proc.on('close', (code) => {
282
- if (code !== 0 && errorMsg) {
283
- resolve({ error: errorMsg });
284
- }
285
- else {
286
- resolve({});
287
- }
288
- });
289
- });
446
+ }
447
+ catch (error) {
448
+ return { error: `Erro ao verificar build: ${error}` };
449
+ }
290
450
  }
291
451
  }
292
452
  exports.HotReloadManager = HotReloadManager;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { HightJSOptions, RequestHandler, BackendRouteConfig, BackendHandler } from './types';
1
+ import { BackendHandler, BackendRouteConfig, HightJSOptions, RequestHandler } from './types';
2
2
  import { HightJSRequest, HightJSResponse } from './api/http';
3
3
  export { HightJSRequest, HightJSResponse };
4
4
  export type { BackendRouteConfig, BackendHandler };
@@ -7,6 +7,7 @@ export { FastifyAdapter } from './adapters/fastify';
7
7
  export { FrameworkAdapterFactory } from './adapters/factory';
8
8
  export type { GenericRequest, GenericResponse, CookieOptions } from './types/framework';
9
9
  export { app } from './helpers';
10
+ export type { WebSocketContext, WebSocketHandler } from './types';
10
11
  export default function hweb(options: HightJSOptions): {
11
12
  prepare: () => Promise<void>;
12
13
  executeInstrumentation: () => void;