@travetto/compiler 4.0.0-rc.0 → 4.0.0-rc.1

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/src/watch.ts CHANGED
@@ -1,24 +1,19 @@
1
- import { readFileSync } from 'node:fs';
2
-
3
- import {
4
- ManifestModuleUtil, ManifestUtil, ManifestModuleFolderType, ManifestModuleFileType, path, ManifestModule,
5
- } from '@travetto/manifest';
1
+ import { ManifestContext, ManifestModuleUtil, ManifestUtil, RuntimeIndex, path } from '@travetto/manifest';
6
2
 
7
3
  import type { CompileStateEntry } from './types';
8
4
  import { CompilerState } from './state';
9
5
  import { CompilerUtil } from './util';
10
6
 
11
- import { WatchEvent, fileWatchEvents } from './internal/watch-core';
7
+ import { AsyncQueue } from '../support/queue';
12
8
 
13
- type DirtyFile = { modFolder: string, mod: string, remove?: boolean, moduleFile: string, folderKey: ManifestModuleFolderType, type: ManifestModuleFileType };
9
+ type WatchAction = 'create' | 'update' | 'delete';
10
+ type WatchEvent = { action: WatchAction, file: string };
11
+ type CompilerWatchEvent = WatchEvent & { entry: CompileStateEntry };
14
12
 
15
13
  /**
16
14
  * Watch support, based on compiler state and manifest details
17
15
  */
18
16
  export class CompilerWatcher {
19
-
20
- #sourceHashes = new Map<string, number>();
21
- #dirtyFiles: DirtyFile[] = [];
22
17
  #state: CompilerState;
23
18
  #signal: AbortSignal;
24
19
 
@@ -27,54 +22,75 @@ export class CompilerWatcher {
27
22
  this.#signal = signal;
28
23
  }
29
24
 
30
- async #rebuildManifestsIfNeeded(): Promise<void> {
31
- if (!this.#dirtyFiles.length) {
25
+ /** Watch files */
26
+ async * #watchFolder(rootPath: string): AsyncIterable<WatchEvent> {
27
+ const q = new AsyncQueue<WatchEvent>(this.#signal);
28
+ const lib = await import('@parcel/watcher');
29
+
30
+ const cleanup = await lib.subscribe(rootPath, (err, events) => {
31
+ if (err) {
32
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
33
+ q.throw(err instanceof Error ? err : new Error((err as Error).message));
34
+ return;
35
+ }
36
+ for (const ev of events) {
37
+ q.add({ action: ev.type, file: path.toPosix(ev.path) });
38
+ }
39
+ }, {
40
+ // TODO: Read .gitignore?
41
+ ignore: [
42
+ 'node_modules', '**/node_modules', '.git', '**/.git',
43
+ `${this.#state.manifest.build.outputFolder}/node_modules/**`,
44
+ `${this.#state.manifest.build.compilerFolder}/node_modules/**`,
45
+ `${this.#state.manifest.build.toolFolder}/**`
46
+ ]
47
+ });
48
+
49
+ if (this.#signal.aborted) { // If already aborted, can happen async
50
+ cleanup.unsubscribe();
51
+ return;
52
+ }
53
+
54
+ this.#signal.addEventListener('abort', () => cleanup.unsubscribe());
55
+
56
+ yield* q;
57
+ }
58
+
59
+ async #rebuildManifestsIfNeeded(event: CompilerWatchEvent, moduleFile: string): Promise<void> {
60
+ if (!event.entry.outputFile || event.action === 'update') {
32
61
  return;
33
62
  }
34
- const mods = [...new Set(this.#dirtyFiles.map(x => x.modFolder))];
35
- const contexts = await Promise.all(mods.map(folder =>
36
- ManifestUtil.getModuleContext(this.#state.manifest, folder)
37
- ));
38
63
 
39
- const files = this.#dirtyFiles.slice(0);
40
- this.#dirtyFiles = [];
64
+ const toUpdate: ManifestContext[] = RuntimeIndex.getDependentModules(event.entry.module.name, 'parents')
65
+ .map(el => ManifestUtil.getModuleContext(this.#state.manifest, el.sourceFolder));
66
+
67
+ toUpdate.push(this.#state.manifest);
68
+
69
+ const mod = event.entry.module;
70
+ const folderKey = ManifestModuleUtil.getFolderKey(moduleFile);
71
+ const fileType = ManifestModuleUtil.getFileType(moduleFile);
41
72
 
42
- for (const ctx of [...contexts, this.#state.manifest]) {
73
+ for (const ctx of toUpdate) {
43
74
  const newManifest = await ManifestUtil.buildManifest(ctx);
44
- for (const file of files) {
45
- if (file.mod in newManifest.modules) {
46
- const modFiles = newManifest.modules[file.mod].files[file.folderKey] ??= [];
47
- const idx = modFiles.findIndex(x => x[0] === file.moduleFile);
48
-
49
- if (!file.remove && idx < 0) {
50
- modFiles.push([file.moduleFile, file.type, Date.now()]);
51
- } else if (idx >= 0) {
52
- if (file.remove) {
53
- modFiles.splice(idx, 1);
54
- } else {
55
- modFiles[idx] = [file.moduleFile, file.type, Date.now()];
56
- }
75
+ if (mod.name in newManifest.modules) {
76
+ const modFiles = newManifest.modules[mod.name].files[folderKey] ??= [];
77
+ const idx = modFiles.findIndex(x => x[0] === moduleFile);
78
+
79
+ if (event.action === 'create' && idx < 0) {
80
+ modFiles.push([moduleFile, fileType, Date.now()]);
81
+ } else if (idx >= 0) {
82
+ if (event.action === 'delete') {
83
+ modFiles.splice(idx, 1);
84
+ } else {
85
+ modFiles[idx] = [moduleFile, fileType, Date.now()];
57
86
  }
58
87
  }
59
88
  }
60
89
  await ManifestUtil.writeManifest(newManifest);
61
90
  }
62
- // Reindex
63
- this.#state.manifestIndex.init(this.#state.manifestIndex.manifestFile);
64
- }
65
-
66
- #getModuleMap(): Record<string, ManifestModule> {
67
- return Object.fromEntries(
68
- Object.values(this.#state.manifest.modules).map(x => [path.resolve(this.#state.manifest.workspace.path, x.sourceFolder), x])
69
- );
70
- }
71
91
 
72
- #addDirtyFile(mod: ManifestModule, folder: string, moduleFile: string, remove = false): void {
73
- this.#dirtyFiles.push({
74
- mod: mod.name, modFolder: folder, remove, moduleFile,
75
- folderKey: ManifestModuleUtil.getFolderKey(moduleFile),
76
- type: ManifestModuleUtil.getFileType(moduleFile),
77
- });
92
+ // Reindex at workspace root
93
+ this.#state.manifestIndex.init(ManifestUtil.getManifestLocation(this.#state.manifest));
78
94
  }
79
95
 
80
96
  /**
@@ -83,69 +99,56 @@ export class CompilerWatcher {
83
99
  * @param handler
84
100
  * @returns
85
101
  */
86
- async * watchChanges(): AsyncIterable<WatchEvent<{ entry: CompileStateEntry }>> {
102
+ async * watchChanges(): AsyncIterable<CompilerWatchEvent> {
87
103
  if (this.#signal.aborted) {
88
- yield* [];
89
104
  return;
90
105
  }
91
106
 
92
- const mods = this.#getModuleMap();
93
-
94
- const modules = [...this.#state.manifestIndex.getModuleList('all')].map(x => this.#state.manifestIndex.getModule(x)!);
95
-
96
- const stream = fileWatchEvents(this.#state.manifest, modules, this.#signal);
97
- for await (const ev of stream) {
98
-
99
- if (ev.action === 'reset') {
100
- yield ev;
101
- return;
107
+ const manifest = this.#state.manifest;
108
+ const ROOT_LOCK = path.resolve(manifest.workspace.path, 'package-lock.json');
109
+ const ROOT_PKG = path.resolve(manifest.workspace.path, 'package.json');
110
+ const OUTPUT_PATH = path.resolve(manifest.workspace.path, manifest.build.outputFolder);
111
+ const COMPILER_PATH = path.resolve(manifest.workspace.path, manifest.build.compilerFolder);
112
+
113
+ for await (const ev of this.#watchFolder(this.#state.manifest.workspace.path)) {
114
+ const { action, file: sourceFile } = ev;
115
+
116
+ if (
117
+ sourceFile === ROOT_LOCK ||
118
+ sourceFile === ROOT_PKG ||
119
+ (action === 'delete' && (sourceFile === OUTPUT_PATH || sourceFile === COMPILER_PATH))
120
+ ) {
121
+ throw new Error('RESET');
102
122
  }
103
123
 
104
- const { action, file: sourceFile, folder } = ev;
105
- const mod = mods[folder];
106
- const moduleFile = mod.sourceFolder ?
107
- (sourceFile.includes(mod.sourceFolder) ? sourceFile.split(`${mod.sourceFolder}/`)[1] : sourceFile) :
108
- sourceFile.replace(`${this.#state.manifest.workspace.path}/`, '');
124
+ const fileType = ManifestModuleUtil.getFileType(sourceFile);
125
+ if (!CompilerUtil.validFile(fileType)) {
126
+ continue;
127
+ }
109
128
 
110
129
  let entry = this.#state.getBySource(sourceFile);
111
130
 
112
- switch (action) {
113
- case 'create': {
114
- const fileType = ManifestModuleUtil.getFileType(moduleFile);
115
- this.#addDirtyFile(mod, folder, moduleFile);
116
- if (CompilerUtil.validFile(fileType)) {
117
- const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
118
- entry = this.#state.registerInput(mod, moduleFile);
119
- this.#sourceHashes.set(sourceFile, hash);
120
- }
121
- break;
122
- }
123
- case 'update': {
124
- if (entry) {
125
- const hash = CompilerUtil.naiveHash(readFileSync(sourceFile, 'utf8'));
126
- if (this.#sourceHashes.get(sourceFile) !== hash) {
127
- this.#state.resetInputSource(entry.input);
128
- this.#sourceHashes.set(sourceFile, hash);
129
- } else {
130
- entry = undefined;
131
- }
132
- }
133
- break;
134
- }
135
- case 'delete': {
136
- if (entry) {
137
- this.#state.removeInput(entry.input);
138
- if (entry.output) {
139
- this.#addDirtyFile(mod, folder, moduleFile, true);
140
- }
141
- }
142
- }
131
+ const mod = entry?.module ?? this.#state.manifestIndex.findModuleForArbitraryFile(sourceFile);
132
+ if (!mod) { // Unknown module
133
+ continue;
143
134
  }
144
135
 
145
- if (entry) {
146
- await this.#rebuildManifestsIfNeeded();
147
- yield { action, file: entry.source, folder, entry };
136
+ const modRoot = mod.sourceFolder || this.#state.manifest.workspace.path;
137
+ const moduleFile = sourceFile.includes(modRoot) ? sourceFile.split(`${modRoot}/`)[1] : sourceFile;
138
+
139
+ if (action === 'create') {
140
+ entry = this.#state.registerInput(mod, moduleFile);
141
+ } else if (!entry) {
142
+ continue;
143
+ } else if (action === 'update' && !this.#state.checkIfSourceChanged(entry.inputFile)) {
144
+ continue;
145
+ } else if (action === 'delete') {
146
+ this.#state.removeInput(entry.inputFile);
148
147
  }
148
+
149
+ const result: CompilerWatchEvent = { action, file: entry.sourceFile, entry };
150
+ await this.#rebuildManifestsIfNeeded(result, moduleFile);
151
+ yield result;
149
152
  }
150
153
  }
151
154
  }
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
 
4
4
  import type { ManifestContext } from '@travetto/manifest';
5
5
 
6
- import type { CompilerEventType, CompilerOp, CompilerServerInfo } from './types';
6
+ import type { CompilerLogLevel, CompilerMode, CompilerServerInfo } from './types';
7
7
  import { LogUtil } from './log';
8
8
  import { CommonUtil } from './util';
9
9
  import { CompilerSetup } from './setup';
@@ -13,9 +13,35 @@ import { CompilerClient } from './server/client';
13
13
 
14
14
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
15
15
  export const main = (ctx: ManifestContext) => {
16
- const client = new CompilerClient(ctx, LogUtil.scoped('client.main'));
16
+ const log = LogUtil.logger('client.main');
17
+ const client = new CompilerClient(ctx, log);
17
18
  const buildFolders = [ctx.build.outputFolder, ctx.build.compilerFolder];
18
19
 
20
+ /** Main entry point for compilation */
21
+ const compile = async (op: CompilerMode, logLevel: CompilerLogLevel, setupOnly = false): Promise<void> => {
22
+ LogUtil.initLogs(ctx, logLevel ?? 'info');
23
+
24
+ const server = await new CompilerServer(ctx, op).listen();
25
+
26
+ // Wait for build to be ready
27
+ if (server) {
28
+ log('debug', 'Start Server');
29
+ await server.processEvents(async function* (signal) {
30
+ const changed = await CompilerSetup.setup(ctx);
31
+ if (!setupOnly) {
32
+ yield* CompilerRunner.runProcess(ctx, changed, op, signal);
33
+ }
34
+ });
35
+ log('debug', 'End Server');
36
+ } else {
37
+ log('info', 'Server already running, waiting for initial compile to complete');
38
+ const ctrl = new AbortController();
39
+ LogUtil.consumeProgressEvents(() => client.fetchEvents('progress', { until: ev => !!ev.complete, signal: ctrl.signal }));
40
+ await client.waitForState(['compile-end', 'watch-start'], 'Successfully built');
41
+ ctrl.abort();
42
+ }
43
+ };
44
+
19
45
  const ops = {
20
46
  /** Stop the server */
21
47
  async stop(): Promise<void> {
@@ -26,6 +52,9 @@ export const main = (ctx: ManifestContext) => {
26
52
  }
27
53
  },
28
54
 
55
+ /** Restart the server */
56
+ async restart(): Promise<void> { await client.stop().then(() => ops.watch()); },
57
+
29
58
  /** Get server info */
30
59
  info: (): Promise<CompilerServerInfo | undefined> => client.info(),
31
60
 
@@ -40,37 +69,33 @@ export const main = (ctx: ManifestContext) => {
40
69
  },
41
70
 
42
71
  /** Stream events */
43
- events: async (type: CompilerEventType, handler: (ev: unknown) => unknown): Promise<void> => {
72
+ events: async (type: string, handler: (ev: unknown) => unknown): Promise<void> => {
44
73
  LogUtil.initLogs(ctx, 'error');
45
- for await (const ev of client.fetchEvents(type)) { await handler(ev); }
74
+ if (type === 'change' || type === 'log' || type === 'progress' || type === 'state') {
75
+ for await (const ev of client.fetchEvents(type)) { await handler(ev); }
76
+ } else {
77
+ throw new Error(`Unknown event type: ${type}`);
78
+ }
46
79
  },
47
80
 
48
- /** Main entry point for compilation */
49
- async compile(op: CompilerOp, setupOnly = false): Promise<(mod: string) => Promise<unknown>> {
50
- LogUtil.initLogs(ctx, op === 'run' ? 'error' : 'info');
81
+ /** Build the project */
82
+ async build(): Promise<void> { await compile('build', 'info'); },
51
83
 
52
- const server = await new CompilerServer(ctx, op).listen();
84
+ /** Build and watch the project */
85
+ async watch(): Promise<void> { await compile('watch', 'info'); },
53
86
 
54
- // Wait for build to be ready
55
- if (server) {
56
- await server.processEvents(async function* (signal) {
57
- const changed = await CompilerSetup.setup(ctx);
58
- if (!setupOnly) {
59
- yield* CompilerRunner.runProcess(ctx, changed, op, signal);
60
- }
61
- });
62
- } else {
63
- const ctrl = new AbortController();
64
- LogUtil.consumeProgressEvents(() => client.fetchEvents('progress', { until: ev => !!ev.complete, signal: ctrl.signal }));
65
- await client.waitForState(['compile-end', 'watch-start'], 'Successfully built');
66
- ctrl.abort();
87
+ /** Build and return a loader */
88
+ async getLoader(): Promise<(mod: string) => Promise<unknown>> {
89
+ // Short circuit if we can
90
+ if (!(await client.isWatching())) {
91
+ await compile('build', 'error');
67
92
  }
68
93
  return CommonUtil.moduleLoader(ctx);
69
94
  },
70
95
 
71
96
  /** Manifest entry point */
72
97
  async manifest(output?: string, prod?: boolean): Promise<void> {
73
- await ops.compile('run', true);
98
+ await compile('build', 'error', true);
74
99
  await CompilerSetup.exportManifest(ctx, output, prod); return;
75
100
  }
76
101
  };
package/support/log.ts CHANGED
@@ -18,6 +18,18 @@ export class LogUtil {
18
18
 
19
19
  static logProgress?: ProgressWriter;
20
20
 
21
+ static linePartial = false;
22
+
23
+ static #rewriteLine(text: string): Promise<void> | void {
24
+ // Move to 1st position, and clear after text
25
+ const done = process.stdout.write(`\x1b[1G${text}\x1b[0K`);
26
+ this.linePartial = !!text;
27
+ if (!done) {
28
+ return new Promise<void>(r => process.stdout.once('drain', r));
29
+ }
30
+ }
31
+
32
+
21
33
  /**
22
34
  * Set level for operation
23
35
  */
@@ -28,20 +40,13 @@ export class LogUtil {
28
40
  this.logLevel = build || defaultLevel;
29
41
  }
30
42
  this.root = ctx.workspace.path;
31
-
32
- if (this.isLevelActive('info') && process.stdout.isTTY) {
33
- this.logProgress = this.#logProgressEvent;
34
- }
43
+ this.logProgress = (this.isLevelActive('info') && process.stdout.isTTY) ? this.#logProgressEvent : undefined;
35
44
  }
36
45
 
37
46
  static #logProgressEvent(ev: CompilerProgressEvent): Promise<void> | void {
38
47
  const pct = Math.trunc(ev.idx * 100 / ev.total);
39
48
  const text = ev.complete ? '' : `Compiling [${'#'.repeat(Math.trunc(pct / 10)).padEnd(10, ' ')}] [${ev.idx}/${ev.total}] ${ev.message}`;
40
- // Move to 1st position, and clear after text
41
- const done = process.stdout.write(`\x1b[1G${text}\x1b[0K`);
42
- if (!done) {
43
- return new Promise<void>(r => process.stdout.once('drain', r));
44
- }
49
+ return this.#rewriteLine(text);
45
50
  }
46
51
 
47
52
  /**
@@ -52,18 +57,18 @@ export class LogUtil {
52
57
  }
53
58
 
54
59
  /**
55
- * Log message with filtering by level
60
+ * Log event with filtering by level
56
61
  */
57
- static log(event: CompilerLogEvent): void;
58
- static log(scope: string, ...args: Parameters<CompilerLogger>): void;
59
- static log(scopeOrEvent: string | CompilerLogEvent, level?: CompilerLogLevel, message?: string, ...args: unknown[]): void {
60
- const ev = typeof scopeOrEvent === 'string' ? { scope: scopeOrEvent, level: level!, message, args } : scopeOrEvent;
62
+ static logEvent(ev: CompilerLogEvent): void {
61
63
  if (this.isLevelActive(ev.level)) {
62
64
  const params = [ev.message, ...ev.args ?? []].map(x => typeof x === 'string' ? x.replaceAll(this.root, '.') : x);
63
65
  if (ev.scope) {
64
66
  params.unshift(`[${ev.scope.padEnd(SCOPE_MAX, ' ')}]`);
65
67
  }
66
68
  params.unshift(new Date().toISOString(), `${ev.level.padEnd(5)}`);
69
+ if (this.linePartial) {
70
+ this.#rewriteLine(''); // Clear out progress line
71
+ }
67
72
  // eslint-disable-next-line no-console
68
73
  console[ev.level]!(...params);
69
74
  }
@@ -73,7 +78,7 @@ export class LogUtil {
73
78
  * With logger
74
79
  */
75
80
  static withLogger<T>(scope: string, op: WithLogger<T>, basic = true): Promise<T> {
76
- const log = this.scoped(scope);
81
+ const log = this.logger(scope);
77
82
  basic && log('debug', 'Started');
78
83
  return op(log).finally(() => basic && log('debug', 'Completed'));
79
84
  }
@@ -81,15 +86,8 @@ export class LogUtil {
81
86
  /**
82
87
  * With scope
83
88
  */
84
- static scoped(scope: string): CompilerLogger {
85
- return this.log.bind(this, scope);
86
- }
87
-
88
- /**
89
- * Stream Compiler log events to console
90
- */
91
- static async consumeLogEvents(src: AsyncIterable<CompilerLogEvent>): Promise<void> {
92
- for await (const ev of src) { this.log(ev); }
89
+ static logger(scope: string): CompilerLogger {
90
+ return (level, message, ...args) => this.logEvent({ scope, message, level, args, time: Date.now() });
93
91
  }
94
92
 
95
93
  /**
package/support/queue.ts CHANGED
@@ -28,6 +28,12 @@ export class AsyncQueue<X> implements AsyncIterator<X>, AsyncIterable<X> {
28
28
  return { value: (this.#queue.length ? this.#queue.shift() : undefined)!, done: this.#done };
29
29
  }
30
30
 
31
+ async throw(e?: Error): Promise<IteratorResult<X>> {
32
+ this.#done = true;
33
+ this.#ready.reject(e);
34
+ return { value: undefined, done: this.#done };
35
+ }
36
+
31
37
  add(item: X): void {
32
38
  this.#queue.push(item);
33
39
  this.#ready.resolve();
@@ -6,10 +6,7 @@ import { ManifestContext } from '@travetto/manifest';
6
6
 
7
7
  import type { CompilerEvent, CompilerEventType, CompilerServerInfo, CompilerStateType } from '../types';
8
8
  import type { CompilerLogger } from '../log';
9
-
10
- declare global {
11
- interface RequestInit { timeout?: number }
12
- }
9
+ import { ProcessHandle } from './process-handle';
13
10
 
14
11
  type FetchEventsConfig<T> = {
15
12
  signal?: AbortSignal;
@@ -23,11 +20,13 @@ type FetchEventsConfig<T> = {
23
20
  export class CompilerClient {
24
21
 
25
22
  #url: string;
26
- #log?: CompilerLogger;
23
+ #log: CompilerLogger;
24
+ #handle: Record<'compiler' | 'server', ProcessHandle>;
27
25
 
28
- constructor(ctx: ManifestContext, log?: CompilerLogger) {
26
+ constructor(ctx: ManifestContext, log: CompilerLogger) {
29
27
  this.#url = ctx.build.compilerUrl.replace('localhost', '127.0.0.1');
30
28
  this.#log = log;
29
+ this.#handle = { compiler: new ProcessHandle(ctx, 'compiler'), server: new ProcessHandle(ctx, 'server') };
31
30
  }
32
31
 
33
32
  toString(): string {
@@ -38,20 +37,49 @@ export class CompilerClient {
38
37
  return this.#url;
39
38
  }
40
39
 
40
+ async #fetch(rel: string, opts?: RequestInit & { timeout?: number }): Promise<Response> {
41
+ const ctrl = new AbortController();
42
+ opts?.signal?.addEventListener('abort', () => ctrl.abort());
43
+ const timeoutId = setTimeout(() => {
44
+ this.#log('error', `Timeout on request to ${this.#url}${rel}`);
45
+ ctrl.abort('TIMEOUT');
46
+ }, 100).unref();
47
+ try {
48
+ return await fetch(`${this.#url}${rel}`, { ...opts, signal: ctrl.signal });
49
+ } finally {
50
+ clearTimeout(timeoutId);
51
+ }
52
+ }
53
+
41
54
  /** Get server information, if server is running */
42
55
  info(): Promise<CompilerServerInfo | undefined> {
43
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
44
- return fetch(`${this.#url}/info`).then(v => v.json(), () => undefined) as Promise<CompilerServerInfo>;
56
+ return this.#fetch('/info').then(v => v.json(), () => undefined)
57
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
58
+ .then(v => v as CompilerServerInfo);
59
+ }
60
+
61
+ async isWatching(): Promise<boolean> {
62
+ return (await this.info())?.state === 'watch-start';
45
63
  }
46
64
 
47
65
  /** Clean the server */
48
66
  clean(): Promise<boolean> {
49
- return fetch(`${this.#url}/clean`).then(v => v.ok, () => false);
67
+ return this.#fetch('/clean').then(v => v.ok, () => false);
50
68
  }
51
69
 
52
- /** Stop server */
53
- stop(): Promise<boolean> {
54
- return fetch(`${this.#url}/stop`).then(v => v.ok, () => false);
70
+ /** Stop server and wait for shutdown */
71
+ async stop(): Promise<boolean> {
72
+ const info = await this.info();
73
+ if (!info) {
74
+ this.#log('debug', 'Stopping server, info not found, manual killing');
75
+ return Promise.all([this.#handle.server.kill(), this.#handle.compiler.kill()])
76
+ .then(v => v.some(x => x));
77
+ }
78
+
79
+ await this.#fetch('/stop').catch(() => { }); // Trigger
80
+ this.#log('debug', 'Waiting for compiler to exit');
81
+ await this.#handle.compiler.ensureKilled();
82
+ return true;
55
83
  }
56
84
 
57
85
  /** Fetch compiler events */
@@ -64,7 +92,7 @@ export class CompilerClient {
64
92
  return;
65
93
  }
66
94
 
67
- this.#log?.('debug', `Starting watch for events of type "${type}"`);
95
+ this.#log('debug', `Starting watch for events of type "${type}"`);
68
96
 
69
97
  let signal = cfg.signal;
70
98
 
@@ -75,17 +103,15 @@ export class CompilerClient {
75
103
  signal = ctrl.signal;
76
104
  }
77
105
 
78
-
79
106
  const { iteration } = info;
80
107
 
81
108
  for (; ;) {
82
109
  const ctrl = new AbortController();
110
+ const quit = (): void => ctrl.abort();
83
111
  try {
84
- signal.addEventListener('abort', () => ctrl.abort());
85
- const stream = await fetch(`${this.#url}/event/${type}`, {
86
- signal: ctrl.signal,
87
- timeout: 1000 * 60 * 60
88
- });
112
+ signal.addEventListener('abort', quit);
113
+ const stream = await this.#fetch(`/event/${type}`, { signal: ctrl.signal, keepalive: true });
114
+
89
115
  for await (const line of rl.createInterface(Readable.fromWeb(stream.body!))) {
90
116
  if (line.trim().charAt(0) === '{') {
91
117
  const val = JSON.parse(line);
@@ -96,17 +122,25 @@ export class CompilerClient {
96
122
  yield val;
97
123
  }
98
124
  }
99
- } catch (err) { }
125
+ } catch (err) {
126
+ if (!ctrl.signal.aborted) { throw err; }
127
+ }
128
+ signal.removeEventListener('abort', quit);
100
129
 
101
130
  await timers.setTimeout(1);
102
131
 
103
132
  info = await this.info();
104
133
 
134
+ if (ctrl.signal.reason === 'TIMEOUT') {
135
+ this.#log('debug', 'Failed due to timeout');
136
+ return;
137
+ }
138
+
105
139
  if (ctrl.signal.aborted || !info || (cfg.enforceIteration && info.iteration !== iteration)) { // If health check fails, or aborted
106
- this.#log?.('debug', `Stopping watch for events of type "${type}"`);
140
+ this.#log('debug', `Stopping watch for events of type "${type}"`);
107
141
  return;
108
142
  } else {
109
- this.#log?.('debug', `Restarting watch for events of type "${type}"`);
143
+ this.#log('debug', `Restarting watch for events of type "${type}"`);
110
144
  }
111
145
  }
112
146
  }
@@ -114,18 +148,12 @@ export class CompilerClient {
114
148
  /** Wait for one of N states to be achieved */
115
149
  async waitForState(states: CompilerStateType[], message?: string, signal?: AbortSignal): Promise<void> {
116
150
  const set = new Set(states);
117
- const existing = await this.info();
118
- this.#log?.('debug', `Existing: ${JSON.stringify(existing)}`);
119
- if (existing && set.has(existing.state)) {
120
- this.#log?.('debug', `Waited for state, ${existing.state} in server info`);
121
- return;
122
- }
123
151
  // Loop until
124
- this.#log?.('debug', `Waiting for states, ${states.join(', ')}`);
152
+ this.#log('debug', `Waiting for states, ${states.join(', ')}`);
125
153
  for await (const _ of this.fetchEvents('state', { signal, until: s => set.has(s.state) })) { }
126
- this.#log?.('debug', `Found state, one of ${states.join(', ')} `);
154
+ this.#log('debug', `Found state, one of ${states.join(', ')} `);
127
155
  if (message) {
128
- this.#log?.('info', message);
156
+ this.#log('info', message);
129
157
  }
130
158
  }
131
159
  }