@noego/app 0.0.13 → 0.0.15

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.
Files changed (2) hide show
  1. package/package.json +2 -1
  2. package/src/commands/dev.js +40 -34
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noego/app",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Production build tool for Dinner/Forge apps.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,7 @@
34
34
  "http-proxy": "^1.18.1",
35
35
  "picomatch": "^2.3.1",
36
36
  "rxjs": "^7.8.1",
37
+ "tree-kill": "^1.2.2",
37
38
  "yaml": "^2.6.0"
38
39
  },
39
40
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { spawn, execSync } from 'node:child_process';
3
+ import { spawn } from 'node:child_process';
4
4
  import { createBuildContext } from '../build/context.js';
5
5
  import { findConfigFile } from '../runtime/index.js';
6
6
  import { loadConfig } from '../runtime/config-loader.js';
@@ -10,39 +10,19 @@ import { debounceTime, filter, exhaustMap, tap, catchError, takeUntil, finalize,
10
10
  import { waitForPortFree } from '../utils/port.js';
11
11
  import { stopProcess, killProcessTree as killProcessTreeUtil } from '../utils/process-observable.js';
12
12
  import { watcherToObservable, FileEventType } from '../utils/file-watcher-observable.js';
13
+ import treeKill from 'tree-kill';
13
14
 
14
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
16
 
16
17
  /**
17
18
  * Kill a process and all its descendants (entire process tree)
18
- * This ensures no orphaned processes remain when killing a parent
19
+ * Uses tree-kill for cross-platform recursive process tree termination
19
20
  */
20
21
  function killProcessTree(pid, signal = 'SIGKILL') {
21
22
  if (!pid) return;
22
23
 
23
24
  try {
24
- // On Unix systems, use pkill to kill all descendants
25
- if (process.platform !== 'win32') {
26
- // Kill all child processes first
27
- try {
28
- execSync(`pkill -P ${pid}`, { stdio: 'ignore' });
29
- } catch (e) {
30
- // No children or already dead, that's fine
31
- }
32
- // Then kill the parent
33
- try {
34
- process.kill(pid, signal);
35
- } catch (e) {
36
- // Process may already be dead
37
- }
38
- } else {
39
- // On Windows, use taskkill with /T flag to kill process tree
40
- try {
41
- execSync(`taskkill /pid ${pid} /T /F`, { stdio: 'ignore' });
42
- } catch (e) {
43
- // Process may already be dead
44
- }
45
- }
25
+ treeKill(pid, signal);
46
26
  } catch (error) {
47
27
  // Ignore errors - process might already be dead
48
28
  }
@@ -500,12 +480,24 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
500
480
  // Auto-restart on crash if under limit
501
481
  if (backendCrashRestarts < MAX_CRASH_RESTARTS) {
502
482
  backendCrashRestarts++;
503
- logger.warn(`[BACKEND] Crash detected. Auto-restart ${backendCrashRestarts}/${MAX_CRASH_RESTARTS} in ${CRASH_RESTART_DELAY}ms...`);
504
- setTimeout(() => {
505
- if (!isShuttingDown) {
506
- startBackend();
483
+ logger.warn(`[BACKEND] Crash detected. Auto-restart ${backendCrashRestarts}/${MAX_CRASH_RESTARTS}...`);
484
+
485
+ // Wait for port to be free before restarting (fixes race condition)
486
+ backendProc = null;
487
+ waitForPortFree(backendPort, 10000, 100).subscribe({
488
+ next: () => {
489
+ if (!isShuttingDown) {
490
+ logger.info(`[BACKEND] Port ${backendPort} is free, restarting...`);
491
+ startBackend();
492
+ }
493
+ },
494
+ error: (err) => {
495
+ logger.warn(`[BACKEND] Port wait warning: ${err.message}. Attempting restart anyway...`);
496
+ if (!isShuttingDown) {
497
+ startBackend();
498
+ }
507
499
  }
508
- }, CRASH_RESTART_DELAY);
500
+ });
509
501
  } else {
510
502
  logger.error(`[BACKEND] Exceeded max crash restarts (${MAX_CRASH_RESTARTS}). Shutting down...`);
511
503
  shutdown('backend-exceeded-restarts', 1, 'backend-crash');
@@ -550,12 +542,24 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
550
542
  // Auto-restart on crash if under limit
551
543
  if (frontendCrashRestarts < MAX_CRASH_RESTARTS) {
552
544
  frontendCrashRestarts++;
553
- logger.warn(`[FRONTEND] Crash detected. Auto-restart ${frontendCrashRestarts}/${MAX_CRASH_RESTARTS} in ${CRASH_RESTART_DELAY}ms...`);
554
- setTimeout(() => {
555
- if (!isShuttingDown) {
556
- startFrontend();
545
+ logger.warn(`[FRONTEND] Crash detected. Auto-restart ${frontendCrashRestarts}/${MAX_CRASH_RESTARTS}...`);
546
+
547
+ // Wait for port to be free before restarting (fixes race condition)
548
+ frontendProc = null;
549
+ waitForPortFree(frontendPort, 10000, 100).subscribe({
550
+ next: () => {
551
+ if (!isShuttingDown) {
552
+ logger.info(`[FRONTEND] Port ${frontendPort} is free, restarting...`);
553
+ startFrontend();
554
+ }
555
+ },
556
+ error: (err) => {
557
+ logger.warn(`[FRONTEND] Port wait warning: ${err.message}. Attempting restart anyway...`);
558
+ if (!isShuttingDown) {
559
+ startFrontend();
560
+ }
557
561
  }
558
- }, CRASH_RESTART_DELAY);
562
+ });
559
563
  } else {
560
564
  logger.error(`[FRONTEND] Exceeded max crash restarts (${MAX_CRASH_RESTARTS}). Shutting down...`);
561
565
  shutdown('frontend-exceeded-restarts', 1, 'frontend-crash');
@@ -762,6 +766,7 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
762
766
  */
763
767
  const setupBackendRestartPipeline = () => {
764
768
  return backendRestartSubject.pipe(
769
+ debounceTime(100), // Batch rapid file saves (5 saves in 1 sec → 1 restart)
765
770
  takeUntil(shutdownSubject),
766
771
  // exhaustMap ignores new restart requests while one is in progress
767
772
  // This prevents restart-during-restart chaos
@@ -798,6 +803,7 @@ async function runSplitServeWithWatch(context, tsxExecutable, tsxArgs, baseEnv,
798
803
  */
799
804
  const setupFrontendRestartPipeline = () => {
800
805
  return frontendRestartSubject.pipe(
806
+ debounceTime(100), // Batch rapid file saves (5 saves in 1 sec → 1 restart)
801
807
  takeUntil(shutdownSubject),
802
808
  // exhaustMap ignores new restart requests while one is in progress
803
809
  exhaustMap((change) => {