@paleo/workspace 0.14.0 → 0.14.2

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.
@@ -33,6 +33,12 @@ export interface DevServerSummaryContext {
33
33
  }[];
34
34
  }
35
35
  export declare function runDevServer(config: DevServerConfig): Promise<void>;
36
+ /**
37
+ * Poll the foreground's own spawn PIDs; when none remain alive, the servers were stopped from
38
+ * outside this process (`dev down`, `down --all`, eviction, or a manual kill). Fires `onStopped`
39
+ * once so the foreground can exit instead of hanging on dead servers.
40
+ */
41
+ export declare function watchForExternalStop(pids: number[], onStopped: () => void, isAlive?: (pid: number) => boolean, intervalMs?: number): NodeJS.Timeout | undefined;
36
42
  export type WorktreeReadyCheck = {
37
43
  ok: true;
38
44
  } | {
@@ -82,17 +82,7 @@ async function start(config, mainWorktree, options) {
82
82
  if (await runStartChecks(config, mainWorktree, ctx, options))
83
83
  return;
84
84
  const state = { spawnPids: {}, startedCallbacks: [] };
85
- try {
86
- await spawnAndAwait(config, ctx, state);
87
- }
88
- catch (err) {
89
- await rollbackStart(state.spawnPids, state.startedCallbacks, ctx);
90
- if (err instanceof StartupError) {
91
- handleStartupFailure(err);
92
- process.exit(1);
93
- }
94
- throw err;
95
- }
85
+ await spawnWithRollback(config, ctx, state);
96
86
  const slot = registerStartedServer(config, mainWorktree, state.spawnPids);
97
87
  printStartSummary(config, slot, state.spawnPids);
98
88
  }
@@ -121,21 +111,22 @@ async function runForeground(config, mainWorktree, options) {
121
111
  process.on("SIGTERM", onSignal);
122
112
  if (await runStartChecks(config, mainWorktree, ctx, options))
123
113
  process.exit(0);
124
- try {
125
- await spawnAndAwait(config, ctx, state);
126
- }
127
- catch (err) {
128
- await rollbackStart(state.spawnPids, state.startedCallbacks, ctx);
129
- if (err instanceof StartupError) {
130
- handleStartupFailure(err);
131
- process.exit(1);
132
- }
133
- throw err;
114
+ // A signal during startup hands teardown to onSignal (rollback + exit 130); block so we
115
+ // neither roll back twice nor register a server that is being torn down.
116
+ if (!(await spawnWithRollback(config, ctx, state, () => shuttingDown))) {
117
+ await new Promise(() => { });
134
118
  }
135
119
  const slot = registerStartedServer(config, mainWorktree, state.spawnPids);
136
120
  started = true;
137
121
  printStartSummary(config, slot, state.spawnPids);
138
122
  tailLogs(config, state.spawnPids);
123
+ watchForExternalStop(Object.values(state.spawnPids), () => {
124
+ if (shuttingDown)
125
+ return;
126
+ shuttingDown = true;
127
+ console.log("\nDev-server stopped externally (e.g. `dev down`). Exiting.");
128
+ process.exit(0);
129
+ });
139
130
  await new Promise(() => { });
140
131
  }
141
132
  async function shutdownForeground(config, mainWorktree) {
@@ -153,6 +144,26 @@ async function runStartChecks(config, mainWorktree, ctx, { evict, restart }) {
153
144
  await checkPortsFree(config.servers, ctx.cwd);
154
145
  return false;
155
146
  }
147
+ /**
148
+ * Spawn and await readiness, rolling back on failure. Returns `false` when `isAborted` reports a
149
+ * concurrent shutdown — the caller must then yield teardown to whoever set it, not proceed.
150
+ */
151
+ async function spawnWithRollback(config, ctx, state, isAborted = () => false) {
152
+ try {
153
+ await spawnAndAwait(config, ctx, state);
154
+ }
155
+ catch (err) {
156
+ if (isAborted())
157
+ return false;
158
+ await rollbackStart(state.spawnPids, state.startedCallbacks, ctx);
159
+ if (err instanceof StartupError) {
160
+ handleStartupFailure(err);
161
+ process.exit(1);
162
+ }
163
+ throw err;
164
+ }
165
+ return !isAborted();
166
+ }
156
167
  async function spawnAndAwait(config, ctx, state) {
157
168
  for (const server of config.servers) {
158
169
  console.log(`Starting ${server.name} dev server...`);
@@ -203,6 +214,7 @@ function printStartSummary(config, slot, spawnPids) {
203
214
  }
204
215
  }
205
216
  const TAIL_INTERVAL_MS = 300;
217
+ const LIVENESS_POLL_MS = 1000;
206
218
  function tailLogs(config, spawnPids) {
207
219
  const names = Object.keys(spawnPids);
208
220
  const prefixed = names.length > 1;
@@ -231,6 +243,22 @@ function followLogFile(path, prefix) {
231
243
  process.stdout.write(prefix === "" ? text : text.replace(/^(?=.)/gm, prefix));
232
244
  }, TAIL_INTERVAL_MS);
233
245
  }
246
+ /**
247
+ * Poll the foreground's own spawn PIDs; when none remain alive, the servers were stopped from
248
+ * outside this process (`dev down`, `down --all`, eviction, or a manual kill). Fires `onStopped`
249
+ * once so the foreground can exit instead of hanging on dead servers.
250
+ */
251
+ export function watchForExternalStop(pids, onStopped, isAlive = isProcessAlive, intervalMs = LIVENESS_POLL_MS) {
252
+ if (pids.length === 0)
253
+ return;
254
+ const timer = setInterval(() => {
255
+ if (pids.some(isAlive))
256
+ return;
257
+ clearInterval(timer);
258
+ onStopped();
259
+ }, intervalMs);
260
+ return timer;
261
+ }
234
262
  async function rollbackStart(spawnPids, startedCallbacks, ctx) {
235
263
  console.error("\nStopping dev servers...");
236
264
  for (const pid of Object.values(spawnPids)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paleo/workspace",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Run multiple git-worktree dev environments side by side.",
5
5
  "keywords": [
6
6
  "workspace",