everything-dev 1.5.0 → 1.7.0

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 (108) hide show
  1. package/dist/api-contract.cjs +55 -8
  2. package/dist/api-contract.cjs.map +1 -1
  3. package/dist/api-contract.mjs +55 -8
  4. package/dist/api-contract.mjs.map +1 -1
  5. package/dist/app.cjs +26 -2
  6. package/dist/app.cjs.map +1 -1
  7. package/dist/app.mjs +27 -3
  8. package/dist/app.mjs.map +1 -1
  9. package/dist/cli/init.cjs +4 -4
  10. package/dist/cli/init.cjs.map +1 -1
  11. package/dist/cli/init.mjs +4 -4
  12. package/dist/cli/init.mjs.map +1 -1
  13. package/dist/cli/sync.cjs +4 -3
  14. package/dist/cli/sync.cjs.map +1 -1
  15. package/dist/cli/sync.mjs +4 -3
  16. package/dist/cli/sync.mjs.map +1 -1
  17. package/dist/cli.cjs +0 -1
  18. package/dist/cli.cjs.map +1 -1
  19. package/dist/cli.mjs +0 -1
  20. package/dist/cli.mjs.map +1 -1
  21. package/dist/components/streaming-view.cjs +0 -18
  22. package/dist/components/streaming-view.cjs.map +1 -1
  23. package/dist/components/streaming-view.mjs +0 -18
  24. package/dist/components/streaming-view.mjs.map +1 -1
  25. package/dist/config.cjs +21 -5
  26. package/dist/config.cjs.map +1 -1
  27. package/dist/config.d.cts +2 -1
  28. package/dist/config.d.cts.map +1 -1
  29. package/dist/config.d.mts +2 -1
  30. package/dist/config.d.mts.map +1 -1
  31. package/dist/config.mjs +21 -6
  32. package/dist/config.mjs.map +1 -1
  33. package/dist/contract.cjs +8 -1
  34. package/dist/contract.cjs.map +1 -1
  35. package/dist/contract.d.cts +44 -8
  36. package/dist/contract.d.cts.map +1 -1
  37. package/dist/contract.d.mts +44 -8
  38. package/dist/contract.d.mts.map +1 -1
  39. package/dist/contract.meta.cjs +1 -1
  40. package/dist/contract.meta.cjs.map +1 -1
  41. package/dist/contract.meta.d.cts +1 -1
  42. package/dist/contract.meta.d.mts +1 -1
  43. package/dist/contract.meta.mjs +1 -1
  44. package/dist/contract.meta.mjs.map +1 -1
  45. package/dist/contract.mjs +8 -1
  46. package/dist/contract.mjs.map +1 -1
  47. package/dist/dev-session.cjs +51 -66
  48. package/dist/dev-session.cjs.map +1 -1
  49. package/dist/dev-session.mjs +52 -67
  50. package/dist/dev-session.mjs.map +1 -1
  51. package/dist/fastkv.cjs +56 -0
  52. package/dist/fastkv.cjs.map +1 -1
  53. package/dist/fastkv.d.cts +45 -1
  54. package/dist/fastkv.d.cts.map +1 -1
  55. package/dist/fastkv.d.mts +45 -1
  56. package/dist/fastkv.d.mts.map +1 -1
  57. package/dist/fastkv.mjs +54 -1
  58. package/dist/fastkv.mjs.map +1 -1
  59. package/dist/host.cjs +1 -1
  60. package/dist/host.cjs.map +1 -1
  61. package/dist/host.mjs +1 -1
  62. package/dist/host.mjs.map +1 -1
  63. package/dist/index.cjs +4 -0
  64. package/dist/index.d.cts +4 -4
  65. package/dist/index.d.mts +4 -4
  66. package/dist/index.mjs +3 -3
  67. package/dist/near-cli.cjs +1 -1
  68. package/dist/near-cli.mjs +1 -1
  69. package/dist/orchestrator.cjs +55 -20
  70. package/dist/orchestrator.cjs.map +1 -1
  71. package/dist/orchestrator.d.cts +5 -4
  72. package/dist/orchestrator.d.cts.map +1 -1
  73. package/dist/orchestrator.d.mts +5 -4
  74. package/dist/orchestrator.d.mts.map +1 -1
  75. package/dist/orchestrator.mjs +55 -20
  76. package/dist/orchestrator.mjs.map +1 -1
  77. package/dist/plugin.cjs +135 -9
  78. package/dist/plugin.cjs.map +1 -1
  79. package/dist/plugin.d.cts +50 -9
  80. package/dist/plugin.d.cts.map +1 -1
  81. package/dist/plugin.d.mts +50 -9
  82. package/dist/plugin.d.mts.map +1 -1
  83. package/dist/plugin.mjs +137 -11
  84. package/dist/plugin.mjs.map +1 -1
  85. package/dist/types.cjs +15 -5
  86. package/dist/types.cjs.map +1 -1
  87. package/dist/types.d.cts +62 -11
  88. package/dist/types.d.cts.map +1 -1
  89. package/dist/types.d.mts +62 -11
  90. package/dist/types.d.mts.map +1 -1
  91. package/dist/types.mjs +15 -5
  92. package/dist/types.mjs.map +1 -1
  93. package/package.json +2 -2
  94. package/src/api-contract.ts +88 -9
  95. package/src/app.ts +55 -7
  96. package/src/cli/init.ts +6 -6
  97. package/src/cli/sync.ts +11 -4
  98. package/src/cli.ts +0 -1
  99. package/src/components/streaming-view.ts +0 -20
  100. package/src/config.ts +39 -23
  101. package/src/contract.meta.ts +4 -1
  102. package/src/contract.ts +7 -0
  103. package/src/dev-session.ts +85 -83
  104. package/src/fastkv.ts +95 -0
  105. package/src/host.ts +1 -1
  106. package/src/orchestrator.ts +61 -31
  107. package/src/plugin.ts +202 -5
  108. package/src/types.ts +38 -4
@@ -1,4 +1,4 @@
1
- import { Effect } from "effect";
1
+ import { Deferred, Effect, Exit } from "effect";
2
2
  import {
3
3
  type DevViewHandle,
4
4
  type LogEntry,
@@ -14,7 +14,7 @@ import {
14
14
  type ProcessCallbacks,
15
15
  type ProcessHandle,
16
16
  } from "./orchestrator";
17
- import { makeProcessRegistry } from "./process-registry";
17
+ import { makeProcessRegistry, type ProcessRegistry } from "./process-registry";
18
18
  import type { BosConfig, RuntimeConfig, SourceMode } from "./types";
19
19
 
20
20
  export interface AppConfig {
@@ -87,9 +87,23 @@ function formatLogLine(entry: LogEntry): string {
87
87
  return `[${ts}] [${entry.source}] [${prefix}] ${entry.line}`;
88
88
  }
89
89
 
90
+ const scopedProcess = (
91
+ pkg: string,
92
+ env: Record<string, string> | undefined,
93
+ callbacks: ProcessCallbacks,
94
+ portOverride: number | undefined,
95
+ bosConfig: BosConfig | undefined,
96
+ runtimeConfig: RuntimeConfig | undefined,
97
+ registry: ProcessRegistry | undefined,
98
+ ) =>
99
+ Effect.acquireRelease(
100
+ makeDevProcess(pkg, env, callbacks, portOverride, bosConfig, runtimeConfig, registry),
101
+ (handle) => handle.kill.pipe(Effect.ignore),
102
+ );
103
+
90
104
  export const runDevSession = (
91
105
  orchestrator: AppOrchestrator,
92
- onCleanupReady?: (cleanup: () => Promise<void>) => void,
106
+ onShutdownReady?: (requestShutdown: () => void) => void,
93
107
  ) =>
94
108
  Effect.gen(function* () {
95
109
  const configDir = getProjectRoot();
@@ -125,62 +139,36 @@ export const runDevSession = (
125
139
  const logger = yield* Effect.promise(() =>
126
140
  createDevLogger(configDir, orchestrator.description),
127
141
  );
128
- const handles: ProcessHandle[] = [];
142
+
143
+ const shutdown = yield* Deferred.make<void>();
144
+
145
+ onShutdownReady?.(() => {
146
+ void Effect.runPromise(Deferred.succeed(shutdown, undefined));
147
+ });
148
+
129
149
  const allLogs: LogEntry[] = [];
130
150
  let view: DevViewHandle | null = null;
131
- let shuttingDown = false;
132
-
133
- const killAll = async () => {
134
- const reversed = [...handles].reverse();
135
- for (const handle of reversed) {
136
- try {
137
- await handle.kill();
138
- } catch {}
139
- }
140
- await Effect.runPromise(registry.killAll(true)).catch(() => {});
141
- };
142
-
143
- const exportLogs = async () => {
144
- console.log("\n");
145
- console.log("═".repeat(70));
146
- console.log(` SESSION LOGS: ${orchestrator.description}`);
147
- console.log(` Started: ${new Date(allLogs[0]?.timestamp || Date.now()).toISOString()}`);
148
- console.log(` Total entries: ${allLogs.length}`);
149
- console.log("═".repeat(70));
150
- console.log("");
151
- for (const entry of allLogs) {
152
- console.log(formatLogLine(entry));
153
- }
154
- console.log("");
155
- console.log("═".repeat(70));
156
- console.log(` Full logs saved to: ${logger.logFile}`);
157
- console.log("═".repeat(70));
158
- console.log("");
159
- };
151
+ let shouldExportLogs = false;
160
152
 
161
- const cleanup = async (showLogs = false) => {
162
- if (shuttingDown) return;
163
- shuttingDown = true;
164
- view?.unmount();
165
- await killAll();
166
- if (showLogs) {
167
- await exportLogs();
168
- }
153
+ const requestShutdownAndExport = () => {
154
+ shouldExportLogs = true;
155
+ void Effect.runPromise(Deferred.succeed(shutdown, undefined));
169
156
  };
170
157
 
171
- onCleanupReady?.(cleanup);
172
-
173
158
  const useInteractive = orchestrator.interactive ?? isInteractiveSupported();
174
159
  view = useInteractive
175
160
  ? renderDevView(
176
161
  initialProcesses,
177
162
  orchestrator.description,
178
163
  orchestrator.env,
179
- () => cleanup(false),
180
- () => cleanup(true),
164
+ () => void Effect.runPromise(Deferred.succeed(shutdown, undefined)),
165
+ requestShutdownAndExport,
181
166
  )
182
- : renderStreamingView(initialProcesses, orchestrator.description, orchestrator.env, () =>
183
- cleanup(false),
167
+ : renderStreamingView(
168
+ initialProcesses,
169
+ orchestrator.description,
170
+ orchestrator.env,
171
+ () => void Effect.runPromise(Deferred.succeed(shutdown, undefined)),
184
172
  );
185
173
 
186
174
  const callbacks: ProcessCallbacks = {
@@ -207,7 +195,7 @@ export const runDevSession = (
207
195
 
208
196
  const startProcess = (pkg: string) => {
209
197
  const portOverride = pkg === "host" ? orchestrator.port : undefined;
210
- return makeDevProcess(
198
+ return scopedProcess(
211
199
  pkg,
212
200
  orchestrator.env,
213
201
  callbacks,
@@ -237,7 +225,6 @@ export const runDevSession = (
237
225
  const hostPackages = orderedPackages.filter((pkg) => pkg === "host");
238
226
 
239
227
  const nonHostHandles = yield* startGroup(nonHostPackages);
240
- handles.push(...nonHostHandles);
241
228
 
242
229
  yield* Effect.forEach(
243
230
  nonHostHandles.map((handle, index) => ({
@@ -249,7 +236,6 @@ export const runDevSession = (
249
236
  );
250
237
 
251
238
  const hostHandles = yield* startGroup(hostPackages);
252
- handles.push(...hostHandles);
253
239
 
254
240
  yield* Effect.forEach(
255
241
  hostHandles.map((handle, index) => ({ handle, pkg: hostPackages[index] ?? handle.name })),
@@ -257,16 +243,48 @@ export const runDevSession = (
257
243
  { concurrency: "unbounded" },
258
244
  );
259
245
 
260
- yield* Effect.addFinalizer(() => Effect.promise(() => cleanup(false)));
261
- yield* Effect.never;
246
+ yield* Effect.addFinalizer(() =>
247
+ Effect.gen(function* () {
248
+ view?.unmount();
249
+
250
+ if (shouldExportLogs) {
251
+ console.log("\n");
252
+ console.log("═".repeat(70));
253
+ console.log(` SESSION LOGS: ${orchestrator.description}`);
254
+ console.log(` Started: ${new Date(allLogs[0]?.timestamp || Date.now()).toISOString()}`);
255
+ console.log(` Total entries: ${allLogs.length}`);
256
+ console.log("═".repeat(70));
257
+ console.log("");
258
+ for (const entry of allLogs) {
259
+ console.log(formatLogLine(entry));
260
+ }
261
+ console.log("");
262
+ console.log("═".repeat(70));
263
+ console.log(` Full logs saved to: ${logger.logFile}`);
264
+ console.log("═".repeat(70));
265
+ console.log("");
266
+ }
267
+
268
+ yield* registry.killAll(true).pipe(Effect.ignore);
269
+ }),
270
+ );
271
+
272
+ yield* Deferred.await(shutdown);
262
273
  });
263
274
 
264
275
  export const startApp = (orchestrator: AppOrchestrator) => {
265
- let activeCleanup: (() => Promise<void>) | null = null;
276
+ let requestShutdown: (() => void) | null = null;
277
+ let signalCount = 0;
278
+ let forceExitTimer: ReturnType<typeof setTimeout> | null = null;
279
+
280
+ const forceExit = () => {
281
+ console.log("\n[Dev] Force exit");
282
+ process.exit(0);
283
+ };
266
284
 
267
285
  const program = Effect.scoped(
268
- runDevSession(orchestrator, (cleanup) => {
269
- activeCleanup = cleanup;
286
+ runDevSession(orchestrator, (shutdown) => {
287
+ requestShutdown = shutdown;
270
288
  }),
271
289
  ).pipe(
272
290
  Effect.catchAll((e) =>
@@ -281,38 +299,22 @@ export const startApp = (orchestrator: AppOrchestrator) => {
281
299
  ),
282
300
  );
283
301
 
284
- const handleSignal = async () => {
285
- if (activeCleanup) await activeCleanup();
286
- };
287
-
288
- const forceExit = () => {
289
- console.log("\n[Dev] Force exit");
290
- process.exit(0);
291
- };
292
-
293
- let signalCount = 0;
294
- process.on("SIGINT", () => {
295
- signalCount++;
296
- if (signalCount > 1) {
297
- forceExit();
298
- return;
299
- }
300
- const timeout = setTimeout(forceExit, 5000);
301
- void handleSignal().finally(() => {
302
- clearTimeout(timeout);
303
- });
304
- });
305
- process.on("SIGTERM", () => {
302
+ const handleSignal = () => {
306
303
  signalCount++;
307
304
  if (signalCount > 1) {
308
305
  forceExit();
309
306
  return;
310
307
  }
311
- const timeout = setTimeout(forceExit, 5000);
312
- void handleSignal().finally(() => {
313
- clearTimeout(timeout);
314
- });
315
- });
308
+ console.log("\n[Dev] Shutting down...");
309
+ forceExitTimer = setTimeout(forceExit, 8000);
310
+ requestShutdown?.();
311
+ };
316
312
 
317
- void Effect.runPromise(program);
313
+ process.on("SIGINT", handleSignal);
314
+ process.on("SIGTERM", handleSignal);
315
+
316
+ Effect.runPromiseExit(program).then((exit) => {
317
+ if (forceExitTimer) clearTimeout(forceExitTimer);
318
+ process.exit(Exit.isSuccess(exit) ? 0 : 0);
319
+ });
318
320
  };
package/src/fastkv.ts CHANGED
@@ -127,6 +127,101 @@ export async function fetchBosConfigFromFastKv<T>(bosUrl: string): Promise<T> {
127
127
  return value as T;
128
128
  }
129
129
 
130
+ export interface PluginManifest {
131
+ schemaVersion: number;
132
+ kind: string;
133
+ plugin: { name: string; version: string };
134
+ runtime: { remoteEntry: string };
135
+ contract: {
136
+ kind: string;
137
+ types: { path: string; exportName: string; typeName: string; sha256: string };
138
+ };
139
+ additionalExports?: Array<{ path: string; exports: string[]; sha256: string }>;
140
+ }
141
+
142
+ export interface PluginMetadata {
143
+ title: string | null;
144
+ description: string | null;
145
+ repoUrl: string | null;
146
+ version: string;
147
+ publishedAt: string;
148
+ cdnUrl: string;
149
+ integrity: string | null;
150
+ }
151
+
152
+ export interface PluginRegistryEntry {
153
+ manifest: PluginManifest;
154
+ metadata: PluginMetadata;
155
+ }
156
+
157
+ export function parsePluginBosUrl(
158
+ source: string,
159
+ ): { accountId: string; pluginName: string } | null {
160
+ if (!source.startsWith("bos://")) return null;
161
+ const match = source.match(/^bos:\/\/([^/]+)\/plugins\/([^/]+)$/);
162
+ if (!match?.[1] || !match[2]) return null;
163
+ return { accountId: match[1], pluginName: match[2] };
164
+ }
165
+
166
+ async function fetchKvValue(accountId: string, key: string): Promise<unknown | null> {
167
+ const payload = await fetchJson<FastKvListResponse>(
168
+ `${getFastKvBaseUrlForAccount(accountId)}/v0/latest/${encodeURIComponent(getRegistryNamespaceForAccount(accountId))}/${encodeURIComponent(accountId)}`,
169
+ {
170
+ method: "POST",
171
+ body: JSON.stringify({ key, limit: 1 }),
172
+ },
173
+ );
174
+ const value = payload?.entries?.find(Boolean)?.value;
175
+ if (value == null) return null;
176
+ if (typeof value === "string") {
177
+ try {
178
+ return JSON.parse(value);
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+ return value;
184
+ }
185
+
186
+ export async function fetchPluginFromRegistry(
187
+ accountId: string,
188
+ pluginName: string,
189
+ ): Promise<PluginRegistryEntry | null> {
190
+ const manifestKey = `plugins/${accountId}/${pluginName}/manifest.json`;
191
+ const metadataKey = `plugins/${accountId}/${pluginName}/metadata`;
192
+
193
+ const [rawManifest, rawMetadata] = await Promise.all([
194
+ fetchKvValue(accountId, manifestKey),
195
+ fetchKvValue(accountId, metadataKey),
196
+ ]);
197
+
198
+ if (!rawManifest || typeof rawManifest !== "object") return null;
199
+
200
+ return {
201
+ manifest: rawManifest as PluginManifest,
202
+ metadata: (rawMetadata ?? {
203
+ title: null,
204
+ description: null,
205
+ repoUrl: null,
206
+ version: "",
207
+ publishedAt: "",
208
+ cdnUrl: "",
209
+ integrity: null,
210
+ }) as PluginMetadata,
211
+ };
212
+ }
213
+
214
+ export async function fetchRemotePluginManifest(cdnUrl: string): Promise<PluginManifest | null> {
215
+ try {
216
+ const baseUrl = cdnUrl.replace(/\/$/, "");
217
+ const response = await fetch(`${baseUrl}/plugin.manifest.json`);
218
+ if (!response.ok) return null;
219
+ return (await response.json()) as PluginManifest;
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
224
+
130
225
  async function fetchJson<T>(url: string, init?: RequestInit): Promise<T | null> {
131
226
  const controller = new AbortController();
132
227
  const timeout = setTimeout(() => controller.abort(), FASTKV_TIMEOUT_MS);
package/src/host.ts CHANGED
@@ -258,7 +258,7 @@ async function runHostServer(opts: {
258
258
  }),
259
259
  );
260
260
 
261
- app.use("*", secureHeaders());
261
+ app.use("*", secureHeaders({ crossOriginOpenerPolicy: "same-origin-allow-popups" }));
262
262
 
263
263
  app.get("/health", (c) => c.text("OK"));
264
264
  app.get("/ready", async (c) => {
@@ -24,7 +24,7 @@ export interface ProcessCallbacks {
24
24
  export interface ProcessHandle {
25
25
  name: string;
26
26
  pid: number | undefined;
27
- kill: () => Promise<void>;
27
+ kill: Effect.Effect<void, unknown>;
28
28
  waitForReady: Effect.Effect<void, Error>;
29
29
  waitForExit: Effect.Effect<unknown>;
30
30
  }
@@ -62,7 +62,6 @@ const processConfigBases: Record<string, ProcessConfigBase> = {
62
62
  command: "bun",
63
63
  args: ["run", "dev"],
64
64
  cwd: "ui",
65
- // Wait for the client build (mf) specifically, not just SSR.
66
65
  readyPatterns: [/\bready\s+built in\b/i, /\bLocal:\b/i, /\bcompiled\b.*successfully/i],
67
66
  errorPatterns: [/error/i, /failed to compile/i],
68
67
  },
@@ -91,6 +90,25 @@ export function getProcessConfig(
91
90
  bosConfig?: BosConfig,
92
91
  runtimeConfig?: RuntimeConfig,
93
92
  ): DevProcess | null {
93
+ if (pkg === "auth") {
94
+ const authConfig = runtimeConfig?.auth;
95
+ if (!authConfig?.localPath || authConfig.source !== "local") return null;
96
+
97
+ const port =
98
+ portOverride ?? authConfig.port ?? (authConfig.url ? parsePort(authConfig.url) : 3020);
99
+
100
+ return {
101
+ name: "auth",
102
+ command: "bun",
103
+ args: ["run", "dev"],
104
+ cwd: authConfig.localPath,
105
+ port,
106
+ readyPatterns: [/ready in/i, /compiled.*successfully/i, /listening/i, /started/i],
107
+ errorPatterns: [/error/i, /failed/i],
108
+ env,
109
+ };
110
+ }
111
+
94
112
  if (pkg.startsWith("plugin:")) {
95
113
  const pluginId = pkg.slice("plugin:".length);
96
114
  const pluginConfig = runtimeConfig?.plugins?.[pluginId] ?? null;
@@ -386,11 +404,14 @@ export const spawnRemoteHost = (
386
404
  return {
387
405
  name: config.name,
388
406
  pid: process.pid,
389
- kill: async () => {
407
+ kill: Effect.gen(function* () {
390
408
  callbacks.onLog(config.name, "Shutting down remote host...");
391
409
  restoreConsole();
392
- await serverHandle.shutdown();
393
- },
410
+ yield* Effect.tryPromise({
411
+ try: () => serverHandle.shutdown(),
412
+ catch: () => {},
413
+ }).pipe(Effect.ignore);
414
+ }),
394
415
  waitForReady: Effect.succeed(undefined),
395
416
  waitForExit: Effect.never,
396
417
  } satisfies ProcessHandle;
@@ -441,14 +462,12 @@ export const spawnDevProcess = (
441
462
  yield* Deferred.succeed(readyDeferred, undefined).pipe(Effect.ignore);
442
463
  });
443
464
 
444
- // Prefer probe-based readiness to avoid brittle log regexes.
445
- // This is best-effort and complements log detection.
446
465
  if (config.port > 0) {
447
466
  const readinessPath =
448
467
  config.name === "host" ? "/health" : config.name === "ui-ssr" ? "/" : "/remoteEntry.js";
449
468
  const url = `http://127.0.0.1:${config.port}${readinessPath}`;
450
469
 
451
- yield* Effect.fork(
470
+ yield* Effect.forkScoped(
452
471
  Effect.gen(function* () {
453
472
  const deadline = Date.now() + 90_000;
454
473
  while (Date.now() < deadline) {
@@ -483,7 +502,7 @@ export const spawnDevProcess = (
483
502
  });
484
503
  }
485
504
 
486
- yield* Effect.fork(
505
+ yield* Effect.forkScoped(
487
506
  Effect.promise(() => proc.exited).pipe(
488
507
  Effect.andThen((code) =>
489
508
  Effect.gen(function* () {
@@ -531,7 +550,7 @@ export const spawnDevProcess = (
531
550
 
532
551
  const decoder = new TextDecoder();
533
552
 
534
- const stdoutFiber = yield* Effect.fork(
553
+ const stdoutFiber = yield* Effect.forkScoped(
535
554
  Effect.async<void>((resume) => {
536
555
  if (!proc.stdout) {
537
556
  resume(Effect.void);
@@ -539,13 +558,13 @@ export const spawnDevProcess = (
539
558
  }
540
559
  const reader = proc.stdout.getReader();
541
560
  let buffer = "";
561
+ let active = true;
542
562
 
543
563
  const pump = (): Promise<void> =>
544
564
  reader.read().then(({ done, value }) => {
565
+ if (!active) return;
545
566
  if (done) {
546
- if (buffer) {
547
- Effect.runSync(handleLine(buffer, false));
548
- }
567
+ if (buffer) Effect.runSync(handleLine(buffer, false));
549
568
  return;
550
569
  }
551
570
  buffer += decoder
@@ -560,11 +579,18 @@ export const spawnDevProcess = (
560
579
  return pump();
561
580
  });
562
581
 
563
- pump().then(() => resume(Effect.void));
582
+ pump().then(() => {
583
+ if (active) resume(Effect.void);
584
+ });
585
+
586
+ return Effect.sync(() => {
587
+ active = false;
588
+ reader.cancel();
589
+ });
564
590
  }),
565
591
  );
566
592
 
567
- const stderrFiber = yield* Effect.fork(
593
+ const stderrFiber = yield* Effect.forkScoped(
568
594
  Effect.async<void>((resume) => {
569
595
  if (!proc.stderr) {
570
596
  resume(Effect.void);
@@ -572,13 +598,13 @@ export const spawnDevProcess = (
572
598
  }
573
599
  const reader = proc.stderr.getReader();
574
600
  let buffer = "";
601
+ let active = true;
575
602
 
576
603
  const pump = (): Promise<void> =>
577
604
  reader.read().then(({ done, value }) => {
605
+ if (!active) return;
578
606
  if (done) {
579
- if (buffer) {
580
- Effect.runSync(handleLine(buffer, true));
581
- }
607
+ if (buffer) Effect.runSync(handleLine(buffer, true));
582
608
  return;
583
609
  }
584
610
  buffer += decoder
@@ -593,25 +619,29 @@ export const spawnDevProcess = (
593
619
  return pump();
594
620
  });
595
621
 
596
- pump().then(() => resume(Effect.void));
622
+ pump().then(() => {
623
+ if (active) resume(Effect.void);
624
+ });
625
+
626
+ return Effect.sync(() => {
627
+ active = false;
628
+ reader.cancel();
629
+ });
597
630
  }),
598
631
  );
599
632
 
600
633
  const handle: ProcessHandle = {
601
634
  name: config.name,
602
635
  pid: proc.pid,
603
- kill: async () => {
604
- const pid = proc.pid;
605
- if (pid) {
606
- await Effect.runPromise(killProcessTree(pid));
607
- } else {
608
- proc.kill("SIGTERM");
609
- await new Promise((r) => setTimeout(r, 100));
610
- try {
611
- proc.kill("SIGKILL");
612
- } catch {}
613
- }
614
- },
636
+ kill: proc.pid
637
+ ? killProcessTree(proc.pid)
638
+ : Effect.gen(function* () {
639
+ proc.kill("SIGTERM");
640
+ yield* Effect.sleep("100 millis");
641
+ try {
642
+ proc.kill("SIGKILL");
643
+ } catch {}
644
+ }),
615
645
  waitForReady: Deferred.await(readyDeferred),
616
646
  waitForExit: Effect.gen(function* () {
617
647
  yield* Fiber.joinAll([stdoutFiber, stderrFiber]);