@electron-stagewright/host-supervisor 0.0.0-stage → 0.1.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -2
  3. package/dist/contract.d.ts +50 -0
  4. package/dist/contract.d.ts.map +1 -0
  5. package/dist/contract.js +2 -0
  6. package/dist/contract.js.map +1 -0
  7. package/dist/env.d.ts +9 -0
  8. package/dist/env.d.ts.map +1 -0
  9. package/dist/env.js +30 -0
  10. package/dist/env.js.map +1 -0
  11. package/dist/errors.d.ts +11 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +33 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/host.d.ts +79 -0
  16. package/dist/host.d.ts.map +1 -0
  17. package/dist/host.js +712 -0
  18. package/dist/host.js.map +1 -0
  19. package/dist/in-process.d.ts +8 -0
  20. package/dist/in-process.d.ts.map +1 -0
  21. package/dist/in-process.js +60 -0
  22. package/dist/in-process.js.map +1 -0
  23. package/dist/index.d.ts +15 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +14 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/policy.d.ts +34 -0
  28. package/dist/policy.d.ts.map +1 -0
  29. package/dist/policy.js +46 -0
  30. package/dist/policy.js.map +1 -0
  31. package/dist/protocol.d.ts +81 -0
  32. package/dist/protocol.d.ts.map +1 -0
  33. package/dist/protocol.js +94 -0
  34. package/dist/protocol.js.map +1 -0
  35. package/dist/serve.d.ts +32 -0
  36. package/dist/serve.d.ts.map +1 -0
  37. package/dist/serve.js +252 -0
  38. package/dist/serve.js.map +1 -0
  39. package/dist/utility.d.ts +43 -0
  40. package/dist/utility.d.ts.map +1 -0
  41. package/dist/utility.js +50 -0
  42. package/dist/utility.js.map +1 -0
  43. package/dist/worker.d.ts +31 -0
  44. package/dist/worker.d.ts.map +1 -0
  45. package/dist/worker.js +50 -0
  46. package/dist/worker.js.map +1 -0
  47. package/package.json +64 -3
package/dist/host.js ADDED
@@ -0,0 +1,712 @@
1
+ /**
2
+ * Supervisor side: starts the host on demand, matches answers to calls, and decides what happens
3
+ * when the host exits on its own.
4
+ */
5
+ import { HOST_ERROR_CODES, HostError } from './errors.js';
6
+ import { PROTOCOL_VERSION, deserializeError, parseHostMessage, } from './protocol.js';
7
+ export const DEFAULT_STOP_GRACE_MS = 10_000;
8
+ export const DEFAULT_RETIRE_WAIT_MS = 5_000;
9
+ /** Rejection that sends a waiting call back to `ensureStarted`. */
10
+ const RETRY = Symbol('retry');
11
+ function unref(timer) {
12
+ ;
13
+ timer.unref?.();
14
+ return timer;
15
+ }
16
+ function raceTimeout(promise, ms) {
17
+ let timer;
18
+ return Promise.race([
19
+ promise.then(() => true),
20
+ new Promise((resolve) => {
21
+ timer = unref(setTimeout(() => resolve(false), ms));
22
+ }),
23
+ ]).finally(() => clearTimeout(timer));
24
+ }
25
+ const HOST_CODES = new Set(HOST_ERROR_CODES);
26
+ function remoteError(serialized) {
27
+ if (serialized.name === 'HostError' && serialized.code && HOST_CODES.has(serialized.code)) {
28
+ return new HostError(serialized.code, serialized.message);
29
+ }
30
+ return deserializeError(serialized);
31
+ }
32
+ function normalizeDecision(decision) {
33
+ if (decision === 'fatal' || decision === 'on-demand' || decision === 'restart') {
34
+ return { action: decision, delayMs: 0 };
35
+ }
36
+ if (typeof decision === 'object' && decision !== null && decision.action === 'restart') {
37
+ const delayMs = Number.isFinite(decision.delayMs) ? Math.max(0, decision.delayMs) : 0;
38
+ return { action: 'restart', delayMs };
39
+ }
40
+ // Anything else is a policy bug; refusing to restart is the safe reading.
41
+ return { action: 'fatal', delayMs: 0 };
42
+ }
43
+ export function createHost(options) {
44
+ const callOps = new Set(options.operations);
45
+ const streamOps = new Set(options.streams ?? []);
46
+ const graceMs = options.stop?.graceMs ?? DEFAULT_STOP_GRACE_MS;
47
+ const retireWaitMs = options.stop?.retireWaitMs ?? DEFAULT_RETIRE_WAIT_MS;
48
+ const maxPending = options.limits?.maxPending ?? Number.POSITIVE_INFINITY;
49
+ const defaultTimeoutMs = options.limits?.callTimeoutMs;
50
+ const onExit = options.onExit ?? (() => 'fatal');
51
+ const now = options.now ?? Date.now;
52
+ let state = 'idle';
53
+ let current = null;
54
+ let starting = null;
55
+ /** The host waiting for its `started` reply, so stop and kill can fail a start that hangs. */
56
+ let booting = null;
57
+ let retiring = null;
58
+ let stopping = null;
59
+ let fatalCause = null;
60
+ let closeRequested = false;
61
+ let wakeDelay = null;
62
+ let nextId = 1;
63
+ const pending = new Map();
64
+ function unavailable() {
65
+ if (state === 'fatal') {
66
+ return new HostError('HOST_FATAL', 'The host exited and will not be restarted', {
67
+ cause: fatalCause,
68
+ });
69
+ }
70
+ return new HostError('HOST_STOPPED', 'The host is stopped');
71
+ }
72
+ /** A restart backoff that stop and kill can cut short. */
73
+ function restartDelay(ms) {
74
+ return new Promise((resolve) => {
75
+ const done = () => {
76
+ clearTimeout(timer);
77
+ wakeDelay = null;
78
+ resolve();
79
+ };
80
+ const timer = unref(setTimeout(done, ms));
81
+ wakeDelay = done;
82
+ });
83
+ }
84
+ // ---- calls ----
85
+ function finish(entry, error, cancel) {
86
+ if (entry.done)
87
+ return;
88
+ entry.done = true;
89
+ pending.delete(entry.id);
90
+ if (entry.timer)
91
+ clearTimeout(entry.timer);
92
+ entry.timer = null;
93
+ entry.detachAbort?.();
94
+ entry.detachAbort = null;
95
+ const life = entry.life;
96
+ if (cancel && life && !life.exited) {
97
+ try {
98
+ life.channel.send({ type: 'cancel', id: entry.id });
99
+ }
100
+ catch {
101
+ // The host is going away; its exit settles the rest.
102
+ }
103
+ }
104
+ const waiter = entry.waiter;
105
+ entry.waiter = null;
106
+ if (!error) {
107
+ // Only a stream reaches here with a waiter: closed early while a pull was pending.
108
+ waiter?.resolve({ done: true, value: undefined });
109
+ return;
110
+ }
111
+ if (waiter)
112
+ waiter.reject(error);
113
+ else
114
+ entry.error = error;
115
+ }
116
+ function armTimer(entry) {
117
+ if (entry.timeoutMs === undefined || entry.timer)
118
+ return;
119
+ entry.timer = unref(setTimeout(() => {
120
+ finish(entry, new HostError('HOST_CALL_TIMEOUT', `The host did not answer in ${entry.timeoutMs} ms`), true);
121
+ }, entry.timeoutMs));
122
+ }
123
+ function admit(stream, callOptions) {
124
+ if (state === 'fatal' || state === 'closed' || state === 'stopping')
125
+ throw unavailable();
126
+ if (pending.size >= maxPending) {
127
+ throw new HostError('HOST_QUEUE_FULL', `More than ${maxPending} calls are pending`);
128
+ }
129
+ const signal = callOptions?.signal;
130
+ if (signal?.aborted) {
131
+ throw new HostError('HOST_CALL_ABORTED', 'The call was aborted', { cause: signal.reason });
132
+ }
133
+ const entry = {
134
+ id: nextId++,
135
+ stream,
136
+ timeoutMs: callOptions?.timeoutMs ?? defaultTimeoutMs,
137
+ life: null,
138
+ done: false,
139
+ error: null,
140
+ waiter: null,
141
+ timer: null,
142
+ detachAbort: null,
143
+ };
144
+ if (signal) {
145
+ const onAbort = () => finish(entry, new HostError('HOST_CALL_ABORTED', 'The call was aborted', { cause: signal.reason }), true);
146
+ signal.addEventListener('abort', onAbort, { once: true });
147
+ entry.detachAbort = () => signal.removeEventListener('abort', onAbort);
148
+ }
149
+ pending.set(entry.id, entry);
150
+ return entry;
151
+ }
152
+ /** Sends the call once a host is running, starting one if needed. */
153
+ function dispatch(entry, message) {
154
+ let ready;
155
+ try {
156
+ ready = ensureStarted();
157
+ }
158
+ catch (error) {
159
+ finish(entry, error, false);
160
+ return;
161
+ }
162
+ ready.then((life) => {
163
+ if (entry.done)
164
+ return;
165
+ if (life.exited || life !== current)
166
+ return dispatch(entry, message);
167
+ entry.life = life;
168
+ try {
169
+ life.channel.send({
170
+ type: 'call',
171
+ id: entry.id,
172
+ op: message.op,
173
+ args: message.args,
174
+ ...(entry.stream ? { stream: true } : {}),
175
+ });
176
+ if (entry.stream)
177
+ pull(entry);
178
+ }
179
+ catch (error) {
180
+ finish(entry, error, false);
181
+ }
182
+ }, (error) => {
183
+ if (entry.done)
184
+ return;
185
+ if (error === RETRY)
186
+ return dispatch(entry, message);
187
+ finish(entry, error, false);
188
+ });
189
+ }
190
+ function pull(entry) {
191
+ const life = entry.life;
192
+ if (!life || entry.done || !entry.waiter)
193
+ return;
194
+ armTimer(entry);
195
+ try {
196
+ life.channel.send({ type: 'next', id: entry.id });
197
+ }
198
+ catch (error) {
199
+ finish(entry, error, false);
200
+ }
201
+ }
202
+ function call(op, args, callOptions) {
203
+ if (!callOps.has(op)) {
204
+ return Promise.reject(new HostError('HOST_OPERATION_REJECTED', `${op} is not a call operation of this host`));
205
+ }
206
+ let entry;
207
+ try {
208
+ entry = admit(false, callOptions);
209
+ }
210
+ catch (error) {
211
+ return Promise.reject(error);
212
+ }
213
+ return new Promise((resolve, reject) => {
214
+ entry.waiter = { resolve, reject };
215
+ armTimer(entry);
216
+ dispatch(entry, { op, args });
217
+ });
218
+ }
219
+ function stream(op, args, callOptions) {
220
+ let entry = null;
221
+ let ended = false;
222
+ let busy = false;
223
+ const done = () => ({ done: true, value: undefined });
224
+ const iterator = {
225
+ [Symbol.asyncIterator]: () => iterator,
226
+ next() {
227
+ if (ended)
228
+ return Promise.resolve(done());
229
+ if (busy) {
230
+ return Promise.reject(new Error('Wait for the previous item before asking for the next'));
231
+ }
232
+ if (!streamOps.has(op)) {
233
+ ended = true;
234
+ return Promise.reject(new HostError('HOST_OPERATION_REJECTED', `${op} is not a stream operation of this host`));
235
+ }
236
+ busy = true;
237
+ return new Promise((resolve, reject) => {
238
+ const waiter = {
239
+ resolve: (value) => {
240
+ busy = false;
241
+ const result = value;
242
+ if (result.done)
243
+ ended = true;
244
+ resolve(result);
245
+ },
246
+ reject: (error) => {
247
+ busy = false;
248
+ ended = true;
249
+ reject(error);
250
+ },
251
+ };
252
+ if (entry === null) {
253
+ try {
254
+ entry = admit(true, callOptions);
255
+ }
256
+ catch (error) {
257
+ waiter.reject(error);
258
+ return;
259
+ }
260
+ entry.waiter = waiter;
261
+ // The first item's timeout also bounds the wait for a host to start.
262
+ armTimer(entry);
263
+ dispatch(entry, { op, args });
264
+ return;
265
+ }
266
+ if (entry.done) {
267
+ if (entry.error)
268
+ waiter.reject(entry.error);
269
+ else
270
+ waiter.resolve(done());
271
+ return;
272
+ }
273
+ entry.waiter = waiter;
274
+ pull(entry);
275
+ });
276
+ },
277
+ return() {
278
+ ended = true;
279
+ if (entry && !entry.done)
280
+ finish(entry, null, true);
281
+ return Promise.resolve(done());
282
+ },
283
+ };
284
+ return iterator;
285
+ }
286
+ // ---- host lifecycle ----
287
+ function onHostMessage(life, raw) {
288
+ const message = parseHostMessage(raw);
289
+ if (!message)
290
+ return;
291
+ switch (message.type) {
292
+ case 'started':
293
+ life.started = true;
294
+ life.startedAt = now();
295
+ life.onStartReply?.(null);
296
+ return;
297
+ case 'start-failed':
298
+ life.onStartReply?.(remoteError(message.error));
299
+ return;
300
+ case 'crashed':
301
+ life.crashError = remoteError(message.error);
302
+ return;
303
+ case 'stats':
304
+ life.lastStats = message.stats;
305
+ // Inside the transport's listener a throw or rejection would escape as uncaught.
306
+ Promise.resolve()
307
+ .then(() => options.onStats?.(message.stats))
308
+ .catch((error) => process.emitWarning(error instanceof Error ? error : new Error(String(error))));
309
+ return;
310
+ case 'stopped':
311
+ life.stoppedReceived = true;
312
+ if (message.error)
313
+ life.stopError = remoteError(message.error);
314
+ life.resolveStopped();
315
+ return;
316
+ case 'result': {
317
+ const entry = pending.get(message.id);
318
+ if (!entry || entry.life !== life)
319
+ return;
320
+ if (!message.ok) {
321
+ finish(entry, remoteError(message.error), false);
322
+ return;
323
+ }
324
+ if (entry.stream)
325
+ return;
326
+ const waiter = entry.waiter;
327
+ entry.waiter = null;
328
+ finish(entry, null, false);
329
+ waiter?.resolve(message.value);
330
+ return;
331
+ }
332
+ case 'item': {
333
+ const entry = pending.get(message.id);
334
+ if (!entry || entry.life !== life || !entry.stream || !entry.waiter)
335
+ return;
336
+ if (entry.timer)
337
+ clearTimeout(entry.timer);
338
+ entry.timer = null;
339
+ const waiter = entry.waiter;
340
+ entry.waiter = null;
341
+ if (message.done)
342
+ finish(entry, null, false);
343
+ waiter.resolve({ done: message.done, value: message.done ? undefined : message.value });
344
+ return;
345
+ }
346
+ }
347
+ }
348
+ function exitError(life) {
349
+ return (life.crashError ??
350
+ life.exit?.error ??
351
+ new Error(`The host exited with code ${String(life.exit?.code ?? null)}`));
352
+ }
353
+ function onHostExit(life, exit) {
354
+ life.exited = true;
355
+ life.exit = exit;
356
+ life.onStartReply?.(exitError(life));
357
+ const stoppingNow = state === 'stopping' || state === 'closed';
358
+ const cause = exitError(life);
359
+ let rejected = 0;
360
+ for (const entry of [...pending.values()]) {
361
+ if (entry.life !== life)
362
+ continue;
363
+ rejected++;
364
+ finish(entry, stoppingNow
365
+ ? new HostError('HOST_STOPPED', 'The host stopped before answering')
366
+ : new HostError('HOST_EXITED', 'The host exited before answering', { cause }), false);
367
+ }
368
+ if (life !== current || state !== 'running')
369
+ return;
370
+ current = null;
371
+ state = 'restarting';
372
+ const recovery = recover({
373
+ kind: exit.outOfMemory ? 'out-of-memory' : 'crash',
374
+ error: cause,
375
+ code: exit.code,
376
+ uptimeMs: Math.max(0, now() - life.startedAt),
377
+ pendingCalls: rejected,
378
+ lastStats: life.lastStats,
379
+ });
380
+ starting = recovery;
381
+ // Registered before any waiter, so a call sent back by RETRY never sees a stale promise.
382
+ recovery.then(() => undefined, () => {
383
+ if (starting === recovery)
384
+ starting = null;
385
+ });
386
+ }
387
+ async function launch() {
388
+ if (retiring)
389
+ await retiring;
390
+ let payload;
391
+ try {
392
+ payload = await options.start?.payload?.();
393
+ }
394
+ catch (error) {
395
+ throw new HostError('HOST_START_FAILED', 'The start payload could not be built', {
396
+ cause: error,
397
+ });
398
+ }
399
+ let channel;
400
+ try {
401
+ channel = options.transport.launch();
402
+ }
403
+ catch (error) {
404
+ throw new HostError('HOST_START_FAILED', 'The host could not be launched', { cause: error });
405
+ }
406
+ let resolveExit;
407
+ let resolveStopped;
408
+ const life = {
409
+ channel,
410
+ started: false,
411
+ startedAt: now(),
412
+ exited: false,
413
+ exit: null,
414
+ exitPromise: new Promise((resolve) => (resolveExit = resolve)),
415
+ crashError: null,
416
+ stoppedReceived: false,
417
+ stoppedPromise: new Promise((resolve) => (resolveStopped = resolve)),
418
+ resolveStopped: () => resolveStopped(),
419
+ stopError: null,
420
+ lastStats: null,
421
+ onStartReply: null,
422
+ };
423
+ channel.onMessage((message) => onHostMessage(life, message));
424
+ channel.onExit((exit) => {
425
+ if (life.exited)
426
+ return;
427
+ onHostExit(life, exit);
428
+ resolveExit();
429
+ });
430
+ const reply = new Promise((resolve, reject) => {
431
+ life.onStartReply = (error) => (error ? reject(error) : resolve());
432
+ });
433
+ let timer;
434
+ const startTimeoutMs = options.start?.timeoutMs;
435
+ booting = life;
436
+ try {
437
+ channel.send({ type: 'start', version: PROTOCOL_VERSION, payload });
438
+ await (startTimeoutMs === undefined
439
+ ? reply
440
+ : Promise.race([
441
+ reply,
442
+ new Promise((_, reject) => {
443
+ timer = unref(setTimeout(() => reject(new Error(`The host did not start in ${startTimeoutMs} ms`)), startTimeoutMs));
444
+ }),
445
+ ]));
446
+ if (life.exited)
447
+ throw exitError(life);
448
+ }
449
+ catch (error) {
450
+ await retire(life);
451
+ throw new HostError('HOST_START_FAILED', 'The host failed to start', { cause: error });
452
+ }
453
+ finally {
454
+ clearTimeout(timer);
455
+ life.onStartReply = null;
456
+ if (booting === life)
457
+ booting = null;
458
+ }
459
+ return life;
460
+ }
461
+ function abortStart() {
462
+ booting?.onStartReply?.(new HostError('HOST_STOPPED', 'The host was stopped while starting'));
463
+ }
464
+ /** Terminates a host and remembers its exit, so the next launch waits for it (bounded). */
465
+ function retire(life) {
466
+ if (life.exited)
467
+ return Promise.resolve();
468
+ try {
469
+ life.channel.kill();
470
+ }
471
+ catch {
472
+ // Already gone; the exit event follows.
473
+ }
474
+ const waiting = Promise.all([retiring, raceTimeout(life.exitPromise, retireWaitMs)]).then(() => {
475
+ if (retiring === waiting)
476
+ retiring = null;
477
+ });
478
+ retiring = waiting;
479
+ return waiting;
480
+ }
481
+ function ensureStarted() {
482
+ if (state === 'fatal' || state === 'closed' || state === 'stopping')
483
+ throw unavailable();
484
+ if (starting)
485
+ return starting;
486
+ if (current && !current.exited)
487
+ return Promise.resolve(current);
488
+ state = 'starting';
489
+ const launching = launch()
490
+ .then((life) => {
491
+ if (life.exited) {
492
+ throw new HostError('HOST_START_FAILED', 'The host exited right after starting', {
493
+ cause: exitError(life),
494
+ });
495
+ }
496
+ return life;
497
+ })
498
+ .then((life) => {
499
+ current = life;
500
+ if (starting === launching)
501
+ starting = null;
502
+ if (state === 'starting')
503
+ state = 'running';
504
+ return life;
505
+ }, (error) => {
506
+ if (starting === launching)
507
+ starting = null;
508
+ if (state === 'starting')
509
+ state = 'idle';
510
+ throw error;
511
+ });
512
+ starting = launching;
513
+ return launching;
514
+ }
515
+ async function recover(exit) {
516
+ let decision;
517
+ try {
518
+ decision = normalizeDecision(await onExit(exit));
519
+ }
520
+ catch {
521
+ decision = { action: 'fatal', delayMs: 0 };
522
+ }
523
+ if (state !== 'restarting')
524
+ throw unavailable();
525
+ if (decision.action === 'fatal') {
526
+ state = 'fatal';
527
+ fatalCause = exit.error;
528
+ starting = null;
529
+ throw unavailable();
530
+ }
531
+ if (decision.action === 'on-demand') {
532
+ state = 'idle';
533
+ starting = null;
534
+ throw RETRY;
535
+ }
536
+ if (decision.delayMs > 0)
537
+ await restartDelay(decision.delayMs);
538
+ if (state !== 'restarting')
539
+ throw unavailable();
540
+ let life;
541
+ try {
542
+ life = await launch();
543
+ if (life.exited)
544
+ throw new Error('The host exited right after starting');
545
+ }
546
+ catch (error) {
547
+ if (state !== 'restarting')
548
+ throw unavailable();
549
+ const cause = error.cause;
550
+ return recover({
551
+ kind: 'start-failed',
552
+ error: cause instanceof Error ? cause : error,
553
+ code: null,
554
+ uptimeMs: null,
555
+ pendingCalls: 0,
556
+ lastStats: null,
557
+ });
558
+ }
559
+ if (state !== 'restarting') {
560
+ // A stop drains the host it finds; a kill expects none left running.
561
+ if (state === 'stopping')
562
+ current = life;
563
+ else
564
+ await retire(life);
565
+ throw unavailable();
566
+ }
567
+ current = life;
568
+ state = 'running';
569
+ starting = null;
570
+ return life;
571
+ }
572
+ async function stopHost() {
573
+ const before = state;
574
+ state = 'stopping';
575
+ wakeDelay?.();
576
+ for (const entry of [...pending.values()]) {
577
+ // Waiting for a start, or a stream between items: nothing in flight to drain.
578
+ if (!entry.life || (entry.stream && !entry.waiter)) {
579
+ finish(entry, new HostError('HOST_STOPPED', 'The host is stopping'), entry.life !== null);
580
+ }
581
+ }
582
+ let failure = null;
583
+ if (starting) {
584
+ const started = starting.then(() => undefined, () => undefined);
585
+ if (!(await raceTimeout(started, graceMs))) {
586
+ abortStart();
587
+ await started;
588
+ failure = new HostError('HOST_STOP_TIMEOUT', `The host did not start within ${graceMs} ms and was terminated`);
589
+ }
590
+ starting = null;
591
+ }
592
+ const life = current;
593
+ if (life && !life.exited) {
594
+ try {
595
+ life.channel.send({ type: 'stop' });
596
+ }
597
+ catch {
598
+ // Treated as a host that never confirmed its stop.
599
+ }
600
+ const answered = Promise.race([life.exitPromise, life.stoppedPromise]);
601
+ if (!(await raceTimeout(answered, graceMs))) {
602
+ await retire(life);
603
+ failure = new HostError('HOST_STOP_TIMEOUT', `The host did not stop within ${graceMs} ms and was terminated`);
604
+ }
605
+ else {
606
+ // A host that confirmed its stop but lingers (a handle left open) is retired quietly.
607
+ if (!(await raceTimeout(life.exitPromise, retireWaitMs)))
608
+ await retire(life);
609
+ if (!life.stoppedReceived) {
610
+ failure = new HostError('HOST_STOP_INCOMPLETE', 'The host exited before finishing its stop');
611
+ }
612
+ else if (life.stopError) {
613
+ failure = new HostError('HOST_STOP_INCOMPLETE', 'The host failed while stopping', {
614
+ cause: life.stopError,
615
+ });
616
+ }
617
+ }
618
+ }
619
+ for (const entry of [...pending.values()]) {
620
+ finish(entry, new HostError('HOST_STOPPED', 'The host stopped before answering'), false);
621
+ }
622
+ current = null;
623
+ state = closeRequested ? 'closed' : before === 'fatal' ? 'fatal' : 'idle';
624
+ if (failure)
625
+ throw failure;
626
+ }
627
+ function shared(final) {
628
+ if (final)
629
+ closeRequested = true;
630
+ if (state === 'closed')
631
+ return Promise.resolve();
632
+ if (stopping)
633
+ return stopping;
634
+ const run = stopHost().finally(() => {
635
+ stopping = null;
636
+ });
637
+ stopping = run;
638
+ return run;
639
+ }
640
+ async function kill() {
641
+ if (state === 'closed')
642
+ return;
643
+ wakeDelay?.();
644
+ abortStart();
645
+ if (state === 'restarting') {
646
+ // The pending restart sees the new state and gives up.
647
+ state = 'idle';
648
+ await starting?.catch(() => undefined);
649
+ return;
650
+ }
651
+ if (state === 'stopping') {
652
+ // Cuts a stop's grace period short; the stop reports how it ended.
653
+ if (current)
654
+ await retire(current);
655
+ await stopping?.catch(() => undefined);
656
+ return;
657
+ }
658
+ if (starting)
659
+ await starting.catch(() => undefined);
660
+ const life = current;
661
+ if (!life)
662
+ return;
663
+ current = null;
664
+ if (state === 'running')
665
+ state = 'idle';
666
+ for (const entry of [...pending.values()]) {
667
+ if (entry.life === life) {
668
+ finish(entry, new HostError('HOST_STOPPED', 'The host was terminated'), false);
669
+ }
670
+ }
671
+ // Calls made from here on wait for this exit before a new host starts.
672
+ await retire(life);
673
+ }
674
+ const ops = new Proxy({}, {
675
+ get(_target, property) {
676
+ if (typeof property !== 'string')
677
+ return undefined;
678
+ if (streamOps.has(property)) {
679
+ return (...args) => stream(property, args);
680
+ }
681
+ if (callOps.has(property)) {
682
+ return (...args) => call(property, args);
683
+ }
684
+ return undefined;
685
+ },
686
+ });
687
+ return {
688
+ ops,
689
+ call: ((op, args, callOptions) => call(op, args, callOptions)),
690
+ stream: ((op, args, callOptions) => stream(op, args, callOptions)),
691
+ async start() {
692
+ for (;;) {
693
+ try {
694
+ await ensureStarted();
695
+ return;
696
+ }
697
+ catch (error) {
698
+ // An `on-demand` exit decision hands the start back to us.
699
+ if (error !== RETRY)
700
+ throw error;
701
+ }
702
+ }
703
+ },
704
+ stop: () => shared(false),
705
+ kill,
706
+ close: () => shared(true),
707
+ get state() {
708
+ return state;
709
+ },
710
+ };
711
+ }
712
+ //# sourceMappingURL=host.js.map