@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.
- package/LICENSE +21 -0
- package/README.md +143 -2
- package/dist/contract.d.ts +50 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -0
- package/dist/env.d.ts +9 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +30 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +33 -0
- package/dist/errors.js.map +1 -0
- package/dist/host.d.ts +79 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +712 -0
- package/dist/host.js.map +1 -0
- package/dist/in-process.d.ts +8 -0
- package/dist/in-process.d.ts.map +1 -0
- package/dist/in-process.js +60 -0
- package/dist/in-process.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/policy.d.ts +34 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +46 -0
- package/dist/policy.js.map +1 -0
- package/dist/protocol.d.ts +81 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +94 -0
- package/dist/protocol.js.map +1 -0
- package/dist/serve.d.ts +32 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +252 -0
- package/dist/serve.js.map +1 -0
- package/dist/utility.d.ts +43 -0
- package/dist/utility.d.ts.map +1 -0
- package/dist/utility.js +50 -0
- package/dist/utility.js.map +1 -0
- package/dist/worker.d.ts +31 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +50 -0
- package/dist/worker.js.map +1 -0
- package/package.json +64 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Electron Stagewright contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,144 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @electron-stagewright/host-supervisor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Runs one stateful host (a database, a native engine, an embedded server) off the Electron main
|
|
4
|
+
thread, behind a closed list of operations, and supervises its life: lazy start, drain on stop,
|
|
5
|
+
per-call timeout and `AbortSignal`, a limit on pending calls, waiting for a killed host to exit
|
|
6
|
+
before the next one starts, and a choice between fatal and bounded restart when it dies.
|
|
7
|
+
|
|
8
|
+
## Worker thread or utility process
|
|
9
|
+
|
|
10
|
+
Choose this first. A native crash (a segfault or abort in a `.node` addon) in a worker thread ends
|
|
11
|
+
the **whole app**; in a `utilityProcess` it ends only the host.
|
|
12
|
+
|
|
13
|
+
| | `worker_threads` (`./worker`) | `utilityProcess` (`./utility`) |
|
|
14
|
+
| --------------------------------- | ----------------------------- | ------------------------------------- |
|
|
15
|
+
| Native crash in the host | Ends the app | Ends the host; the supervisor decides |
|
|
16
|
+
| JavaScript exception, heap limit | Ends the host | Ends the host |
|
|
17
|
+
| Start to first answer | 19 ms | 59 ms |
|
|
18
|
+
| Call round trip | 24 µs | 37 µs |
|
|
19
|
+
| Memory | Counted in the main process | Its own process, 67 MB working set |
|
|
20
|
+
| Runs without Electron (tests, CI) | Yes | No |
|
|
21
|
+
|
|
22
|
+
Start, round trip and memory: medians of 7 rounds on one Apple M4 Max, Electron 42.11.10, with a
|
|
23
|
+
host that loads nothing. They are indicative only. In one app that moved an embedded server off the
|
|
24
|
+
main thread, the worker added about 21 MB and left 0.7 % of the main thread blocked under load;
|
|
25
|
+
a utility process added 46 MB and 3.6 %.
|
|
26
|
+
|
|
27
|
+
Use a worker when the host runs JavaScript or native code you trust not to crash. Use a utility
|
|
28
|
+
process when it loads a native engine (DuckDB, a driver, a codec) whose crash must not take the
|
|
29
|
+
window with it.
|
|
30
|
+
|
|
31
|
+
## Use
|
|
32
|
+
|
|
33
|
+
The contract is an interface shared by both sides:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
export interface DbOps {
|
|
37
|
+
query(sql: string): Promise<unknown[]>
|
|
38
|
+
rows(sql: string): AsyncIterable<unknown[]>
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
In the main process:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { fileURLToPath } from 'node:url'
|
|
46
|
+
|
|
47
|
+
import { createHost, restartPolicy } from '@electron-stagewright/host-supervisor'
|
|
48
|
+
import { utilityProcessTransport } from '@electron-stagewright/host-supervisor/utility'
|
|
49
|
+
|
|
50
|
+
const host = createHost<DbOps, { key: string }>({
|
|
51
|
+
operations: ['query'],
|
|
52
|
+
streams: ['rows'],
|
|
53
|
+
transport: utilityProcessTransport({
|
|
54
|
+
entry: fileURLToPath(new URL('./db-host.js', import.meta.url)),
|
|
55
|
+
serviceName: 'Database',
|
|
56
|
+
}),
|
|
57
|
+
start: { payload: () => ({ key: readKey() }) },
|
|
58
|
+
stop: { graceMs: 30_000, retireWaitMs: 5_000 },
|
|
59
|
+
limits: { maxPending: 64, callTimeoutMs: 30_000 },
|
|
60
|
+
onExit: restartPolicy({
|
|
61
|
+
isFatal: (exit) => (exit.error as { code?: string }).code === 'SQLITE_CORRUPT',
|
|
62
|
+
}),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
await host.ops.query('select 1')
|
|
66
|
+
await host.call('query', ['select 2'], { signal })
|
|
67
|
+
for await (const chunk of host.ops.rows('select * from big')) render(chunk)
|
|
68
|
+
await host.close()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
In the host entry (`db-host.js`):
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { serveHost } from '@electron-stagewright/host-supervisor'
|
|
75
|
+
|
|
76
|
+
let db: Db | null = null
|
|
77
|
+
serveHost<DbOps, { key: string }>({
|
|
78
|
+
operations: ['query'],
|
|
79
|
+
streams: ['rows'],
|
|
80
|
+
start: async ({ key }) => {
|
|
81
|
+
db = await open(key)
|
|
82
|
+
return {
|
|
83
|
+
query: (sql, { signal }) => db!.all(sql, { signal }),
|
|
84
|
+
rows: (sql) => db!.iterate(sql),
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
stop: () => db?.close(),
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`serveHost` finds its port: Electron's `process.parentPort` in a utility process, the
|
|
92
|
+
`worker_threads` parent port in a worker. The entry never imports Electron.
|
|
93
|
+
|
|
94
|
+
## Behaviour
|
|
95
|
+
|
|
96
|
+
- **Start.** The host starts on the first call, or on `host.start()`. The payload is the first
|
|
97
|
+
message; keep secrets there, not in env or argv. The utility transport passes
|
|
98
|
+
`minimalEnvironment(process.env)` unless you pass `env`.
|
|
99
|
+
- **Calls.** Each call has `limits.callTimeoutMs` (or `timeoutMs`) and an optional `signal`. A
|
|
100
|
+
timeout or an abort rejects the call and aborts `context.signal` in the handler, which receives
|
|
101
|
+
it after the caller's arguments. Past `maxPending` calls and open streams, a call fails with
|
|
102
|
+
`HOST_QUEUE_FULL`.
|
|
103
|
+
- **Streams.** One item per pull; nothing is produced ahead. Leaving a `for await` early closes the
|
|
104
|
+
producer in the host.
|
|
105
|
+
- **Exits.** A call in flight when the host exits fails with `HOST_EXITED`; its `cause` is the
|
|
106
|
+
host's uncaught error when the host could report it. It is never resent. `onExit` then returns
|
|
107
|
+
`fatal` (the default), `on-demand` or `restart`. `restartPolicy()` restarts up to 3 times in 10
|
|
108
|
+
minutes with 0.5, 2 and 8 s of backoff, and a restart whose start fails counts.
|
|
109
|
+
- **Stop.** `stop()` refuses new calls, lets accepted ones answer, runs the host's `stop`, and
|
|
110
|
+
terminates the host after `graceMs`. It rejects with `HOST_STOP_TIMEOUT` or
|
|
111
|
+
`HOST_STOP_INCOMPLETE` when the host did not close cleanly, so code that is about to touch its
|
|
112
|
+
files can stop. `kill()` terminates without draining. `close()` stops and refuses every later
|
|
113
|
+
call. After `stop()` or `kill()` the next call starts a new host, once the old one has exited or
|
|
114
|
+
`retireWaitMs` has passed.
|
|
115
|
+
- **Memory.** Worker `resourceLimits` usually end only the worker, reported as `out-of-memory`;
|
|
116
|
+
one allocation far past the cap can still abort the whole process, so use `utilityProcess` when
|
|
117
|
+
the app must survive its host. A `utilityProcess` that runs out of heap is reported as a
|
|
118
|
+
`crash`: Electron gives no out-of-memory signal. Native memory sits outside any heap cap: pass
|
|
119
|
+
`statsIntervalMs` to `serveHost`, and `memoryWatch({ rssMb, onAlert })` as `onStats`. A worker
|
|
120
|
+
shares the process, so its `rssMb` is the whole app's. `heapLimitMb` is V8's limit, which adds
|
|
121
|
+
the young generation to `maxOldGenerationSizeMb`. An `onStats` that throws or rejects becomes a
|
|
122
|
+
process warning.
|
|
123
|
+
|
|
124
|
+
Errors keep their message and a code matching `^[A-Z][A-Z0-9_]{1,80}$`; stacks and object graphs
|
|
125
|
+
never cross. The supervisor's own failures are `HostError` with a code from `HOST_ERROR_CODES`.
|
|
126
|
+
|
|
127
|
+
## Platform notes
|
|
128
|
+
|
|
129
|
+
- `terminate()` cannot interrupt a synchronous native call: the grace period is honoured between
|
|
130
|
+
calls, not inside one.
|
|
131
|
+
- Large results should stream. `postMessage` copies by structured clone, and Electron's V8 memory
|
|
132
|
+
cage rules out external ArrayBuffers, so a native addon must copy into V8 memory anyway.
|
|
133
|
+
- Windows has no POSIX signals; a kill is abrupt and a file can stay locked until the process is
|
|
134
|
+
gone. That is what `retireWaitMs` waits for.
|
|
135
|
+
- The package ships no binaries. Unpack each `.node` from the asar, sign every `.node` and `.dylib`
|
|
136
|
+
(the macOS hardened runtime refuses unsigned libraries), and load them in a packaged smoke test.
|
|
137
|
+
- On Linux a utility process inherits the app's sandbox settings; test it packaged, not only with
|
|
138
|
+
`--no-sandbox`.
|
|
139
|
+
|
|
140
|
+
Design record: [ADR-032](../../docs/adr/032-host-supervisor-library.md).
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { SupervisorMessage } from './protocol.js';
|
|
2
|
+
/** What a handler receives after the caller's arguments. */
|
|
3
|
+
export interface CallContext {
|
|
4
|
+
/** Aborted when the caller aborts, the call times out, or the stream is closed early. */
|
|
5
|
+
readonly signal: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
type FixedArity<A extends readonly unknown[]> = number extends A['length'] ? false : A extends Required<A> ? true : false;
|
|
8
|
+
/**
|
|
9
|
+
* Every member is an operation that returns a promise or an async iterable. Parameters are fixed:
|
|
10
|
+
* the handler's context follows the caller's arguments, so optional and rest parameters would
|
|
11
|
+
* shift it. Declare an optional value as `T | undefined` instead.
|
|
12
|
+
*/
|
|
13
|
+
export type Contract<Ops> = {
|
|
14
|
+
[K in keyof Ops]: Ops[K] extends (...args: infer A) => infer R ? FixedArity<A> extends true ? R extends Promise<unknown> | AsyncIterable<unknown> ? Ops[K] : (...args: never[]) => Promise<unknown> | AsyncIterable<unknown> : (...args: never[]) => 'optional and rest parameters are not supported' : never;
|
|
15
|
+
};
|
|
16
|
+
export type ArgsOf<F> = F extends (...args: infer A) => unknown ? A : never;
|
|
17
|
+
/** Operations that answer once. */
|
|
18
|
+
export type CallOperation<Ops> = {
|
|
19
|
+
[K in keyof Ops]: Ops[K] extends (...args: never[]) => Promise<unknown> ? K : never;
|
|
20
|
+
}[keyof Ops] & string;
|
|
21
|
+
/** Operations that answer with a stream, pulled one item at a time. */
|
|
22
|
+
export type StreamOperation<Ops> = {
|
|
23
|
+
[K in keyof Ops]: Ops[K] extends (...args: never[]) => AsyncIterable<unknown> ? K : never;
|
|
24
|
+
}[keyof Ops] & string;
|
|
25
|
+
/** The host-side implementation of a contract: same arguments, plus the call context. */
|
|
26
|
+
export type Handlers<Ops> = {
|
|
27
|
+
[K in keyof Ops]: Ops[K] extends (...args: infer A) => Promise<infer R> ? (...args: [...A, CallContext]) => R | Promise<R> : Ops[K] extends (...args: infer A) => AsyncIterable<infer T> ? (...args: [...A, CallContext]) => AsyncIterable<T> | Iterable<T> : never;
|
|
28
|
+
};
|
|
29
|
+
/** How a channel ended, as the transport saw it. */
|
|
30
|
+
export interface ChannelExit {
|
|
31
|
+
readonly code: number | null;
|
|
32
|
+
readonly error?: Error;
|
|
33
|
+
/** The runtime ended the host for exceeding its memory limit. */
|
|
34
|
+
readonly outOfMemory?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** One running host, as seen from the supervisor. */
|
|
37
|
+
export interface HostChannel {
|
|
38
|
+
send(message: SupervisorMessage): void;
|
|
39
|
+
onMessage(listener: (message: unknown) => void): void;
|
|
40
|
+
/** Called once, when the thread or process has exited. */
|
|
41
|
+
onExit(listener: (exit: ChannelExit) => void): void;
|
|
42
|
+
/** Ends the host without waiting for it; `onExit` still fires. */
|
|
43
|
+
kill(): void;
|
|
44
|
+
}
|
|
45
|
+
/** Starts hosts. A worker thread, a utility process, or anything that speaks the protocol. */
|
|
46
|
+
export interface HostTransport {
|
|
47
|
+
launch(): HostChannel;
|
|
48
|
+
}
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":""}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Only what a runtime needs to start. Secrets go in the start payload, never in env or argv.
|
|
3
|
+
* `home` replaces HOME and USERPROFILE, so a library's `~/.config` defaults stay in the app's folder.
|
|
4
|
+
*/
|
|
5
|
+
export declare function minimalEnvironment(env: NodeJS.ProcessEnv, options?: {
|
|
6
|
+
readonly home?: string;
|
|
7
|
+
readonly passThrough?: readonly string[];
|
|
8
|
+
}): Record<string, string>;
|
|
9
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAaA;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,OAAO,GAAE;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAO,GACjF,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWxB"}
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const PASSED_THROUGH = [
|
|
2
|
+
'PATH',
|
|
3
|
+
'TMPDIR',
|
|
4
|
+
'TMP',
|
|
5
|
+
'TEMP',
|
|
6
|
+
'LANG',
|
|
7
|
+
'LC_ALL',
|
|
8
|
+
'LC_CTYPE',
|
|
9
|
+
'TZ',
|
|
10
|
+
'SystemRoot',
|
|
11
|
+
'windir',
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Only what a runtime needs to start. Secrets go in the start payload, never in env or argv.
|
|
15
|
+
* `home` replaces HOME and USERPROFILE, so a library's `~/.config` defaults stay in the app's folder.
|
|
16
|
+
*/
|
|
17
|
+
export function minimalEnvironment(env, options = {}) {
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const name of [...PASSED_THROUGH, ...(options.passThrough ?? [])]) {
|
|
20
|
+
const value = env[name];
|
|
21
|
+
if (value !== undefined)
|
|
22
|
+
out[name] = value;
|
|
23
|
+
}
|
|
24
|
+
if (options.home !== undefined) {
|
|
25
|
+
out['HOME'] = options.home;
|
|
26
|
+
out['USERPROFILE'] = options.home;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=env.js.map
|
package/dist/env.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,MAAM,cAAc,GAAG;IACrB,MAAM;IACN,QAAQ;IACR,KAAK;IACL,MAAM;IACN,MAAM;IACN,QAAQ;IACR,UAAU;IACV,IAAI;IACJ,YAAY;IACZ,QAAQ;CACA,CAAA;AAEV;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAAsB,EACtB,UAAgF,EAAE;IAElF,MAAM,GAAG,GAA2B,EAAE,CAAA;IACtC,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,cAAc,EAAE,GAAG,CAAC,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;QACvE,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,CAAA;QACvB,IAAI,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAA;IAC5C,CAAC;IACD,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC/B,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAA;QAC1B,GAAG,CAAC,aAAa,CAAC,GAAG,OAAO,CAAC,IAAI,CAAA;IACnC,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The supervisor's own failures. Application errors keep their own code when it is well formed. */
|
|
2
|
+
export declare const HOST_ERROR_CODES: readonly ["HOST_STOPPED", "HOST_EXITED", "HOST_START_FAILED", "HOST_FATAL", "HOST_CALL_TIMEOUT", "HOST_CALL_ABORTED", "HOST_QUEUE_FULL", "HOST_OPERATION_REJECTED", "HOST_STOP_TIMEOUT", "HOST_STOP_INCOMPLETE"];
|
|
3
|
+
export type HostErrorCode = (typeof HOST_ERROR_CODES)[number];
|
|
4
|
+
export declare class HostError extends Error {
|
|
5
|
+
readonly code: HostErrorCode;
|
|
6
|
+
constructor(code: HostErrorCode, message: string, options?: {
|
|
7
|
+
cause?: unknown;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export declare function isHostError(error: unknown, code?: HostErrorCode): error is HostError;
|
|
11
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,eAAO,MAAM,gBAAgB,kNAmBnB,CAAA;AAEV,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE7D,qBAAa,SAAU,SAAQ,KAAK;IAClC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;gBAEhB,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKhF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,KAAK,IAAI,SAAS,CAEpF"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** The supervisor's own failures. Application errors keep their own code when it is well formed. */
|
|
2
|
+
export const HOST_ERROR_CODES = [
|
|
3
|
+
/** `close()` was called, or the call was still waiting when the host stopped. */
|
|
4
|
+
'HOST_STOPPED',
|
|
5
|
+
/** The host exited while the call was in flight. Calls are never resent. */
|
|
6
|
+
'HOST_EXITED',
|
|
7
|
+
/** The host failed to start, or answered `start` with a failure. */
|
|
8
|
+
'HOST_START_FAILED',
|
|
9
|
+
/** `onExit` chose `fatal`; the host never starts again. */
|
|
10
|
+
'HOST_FATAL',
|
|
11
|
+
'HOST_CALL_TIMEOUT',
|
|
12
|
+
'HOST_CALL_ABORTED',
|
|
13
|
+
/** More than `maxPending` calls were waiting. */
|
|
14
|
+
'HOST_QUEUE_FULL',
|
|
15
|
+
/** The host refused an operation outside its closed list. */
|
|
16
|
+
'HOST_OPERATION_REJECTED',
|
|
17
|
+
/** The host did not finish closing within the grace period and was terminated. */
|
|
18
|
+
'HOST_STOP_TIMEOUT',
|
|
19
|
+
/** The host exited during `stop` without confirming that its cleanup finished. */
|
|
20
|
+
'HOST_STOP_INCOMPLETE',
|
|
21
|
+
];
|
|
22
|
+
export class HostError extends Error {
|
|
23
|
+
code;
|
|
24
|
+
constructor(code, message, options) {
|
|
25
|
+
super(message, options);
|
|
26
|
+
this.name = 'HostError';
|
|
27
|
+
this.code = code;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function isHostError(error, code) {
|
|
31
|
+
return error instanceof HostError && (code === undefined || error.code === code);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,iFAAiF;IACjF,cAAc;IACd,4EAA4E;IAC5E,aAAa;IACb,oEAAoE;IACpE,mBAAmB;IACnB,2DAA2D;IAC3D,YAAY;IACZ,mBAAmB;IACnB,mBAAmB;IACnB,iDAAiD;IACjD,iBAAiB;IACjB,6DAA6D;IAC7D,yBAAyB;IACzB,kFAAkF;IAClF,mBAAmB;IACnB,kFAAkF;IAClF,sBAAsB;CACd,CAAA;AAIV,MAAM,OAAO,SAAU,SAAQ,KAAK;IACzB,IAAI,CAAe;IAE5B,YAAY,IAAmB,EAAE,OAAe,EAAE,OAA6B;QAC7E,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,WAAW,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;CACF;AAED,MAAM,UAAU,WAAW,CAAC,KAAc,EAAE,IAAoB;IAC9D,OAAO,KAAK,YAAY,SAAS,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AAClF,CAAC"}
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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 type { ArgsOf, CallOperation, Contract, HostTransport, StreamOperation } from './contract.js';
|
|
6
|
+
import { type HostStats } from './protocol.js';
|
|
7
|
+
export declare const DEFAULT_STOP_GRACE_MS = 10000;
|
|
8
|
+
export declare const DEFAULT_RETIRE_WAIT_MS = 5000;
|
|
9
|
+
export type HostState = 'idle' | 'starting' | 'running' | 'restarting' | 'stopping' | 'fatal' | 'closed';
|
|
10
|
+
/** What the supervisor knows about a host that exited without being asked to. */
|
|
11
|
+
export interface HostExit {
|
|
12
|
+
/** `start-failed` is a restart whose start failed; the first start rejects its callers instead. */
|
|
13
|
+
readonly kind: 'crash' | 'out-of-memory' | 'start-failed';
|
|
14
|
+
readonly error: Error;
|
|
15
|
+
readonly code: number | null;
|
|
16
|
+
/** Milliseconds between `started` and the exit; null when the host never started. */
|
|
17
|
+
readonly uptimeMs: number | null;
|
|
18
|
+
/** Calls the exit rejected with `HOST_EXITED`. */
|
|
19
|
+
readonly pendingCalls: number;
|
|
20
|
+
readonly lastStats: HostStats | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* - `fatal`: refuse every call from now on.
|
|
24
|
+
* - `on-demand`: start a new host on the next call.
|
|
25
|
+
* - `restart`: start a new host now, after `delayMs`.
|
|
26
|
+
*/
|
|
27
|
+
export type ExitDecision = 'fatal' | 'on-demand' | 'restart' | {
|
|
28
|
+
readonly action: 'restart';
|
|
29
|
+
readonly delayMs: number;
|
|
30
|
+
};
|
|
31
|
+
export interface CallOptions {
|
|
32
|
+
readonly signal?: AbortSignal;
|
|
33
|
+
/** Overrides `limits.callTimeoutMs`; for a stream it bounds each item. */
|
|
34
|
+
readonly timeoutMs?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface HostOptions<Ops extends Contract<Ops>, Payload = undefined> {
|
|
37
|
+
/** The closed list of operations that answer once. */
|
|
38
|
+
readonly operations: readonly CallOperation<Ops>[];
|
|
39
|
+
/** The closed list of operations that answer with a stream. */
|
|
40
|
+
readonly streams?: readonly StreamOperation<Ops>[];
|
|
41
|
+
readonly transport: HostTransport;
|
|
42
|
+
readonly start?: {
|
|
43
|
+
/** Sent with `start` on every launch. Keeps secrets out of env and argv. */
|
|
44
|
+
readonly payload?: () => Payload | Promise<Payload>;
|
|
45
|
+
readonly timeoutMs?: number;
|
|
46
|
+
};
|
|
47
|
+
readonly stop?: {
|
|
48
|
+
/** How long `stop()` lets the host drain and close before terminating it. */
|
|
49
|
+
readonly graceMs?: number;
|
|
50
|
+
/** How long a terminated host may take to exit before the next one starts anyway. */
|
|
51
|
+
readonly retireWaitMs?: number;
|
|
52
|
+
};
|
|
53
|
+
readonly limits?: {
|
|
54
|
+
/** Calls and open streams allowed at once; one more fails with `HOST_QUEUE_FULL`. */
|
|
55
|
+
readonly maxPending?: number;
|
|
56
|
+
readonly callTimeoutMs?: number;
|
|
57
|
+
};
|
|
58
|
+
/** Decides what follows an exit nobody asked for. Defaults to `fatal`. */
|
|
59
|
+
readonly onExit?: (exit: HostExit) => ExitDecision | Promise<ExitDecision>;
|
|
60
|
+
readonly onStats?: (stats: HostStats) => void;
|
|
61
|
+
readonly now?: () => number;
|
|
62
|
+
}
|
|
63
|
+
export interface Host<Ops extends Contract<Ops>> {
|
|
64
|
+
/** One method per operation, with the contract's own signature. */
|
|
65
|
+
readonly ops: Ops;
|
|
66
|
+
call<K extends CallOperation<Ops>>(op: K, args: ArgsOf<Ops[K]>, options?: CallOptions): ReturnType<Ops[K]>;
|
|
67
|
+
stream<K extends StreamOperation<Ops>>(op: K, args: ArgsOf<Ops[K]>, options?: CallOptions): ReturnType<Ops[K]>;
|
|
68
|
+
/** Starts the host now instead of on the first call. */
|
|
69
|
+
start(): Promise<void>;
|
|
70
|
+
/** Drains and stops the host. The next call starts a new one. */
|
|
71
|
+
stop(): Promise<void>;
|
|
72
|
+
/** Terminates the host without draining, and waits (bounded) for it to exit. Never throws. */
|
|
73
|
+
kill(): Promise<void>;
|
|
74
|
+
/** Stops the host and refuses every later call. */
|
|
75
|
+
close(): Promise<void>;
|
|
76
|
+
readonly state: HostState;
|
|
77
|
+
}
|
|
78
|
+
export declare function createHost<Ops extends Contract<Ops>, Payload = undefined>(options: HostOptions<Ops, Payload>): Host<Ops>;
|
|
79
|
+
//# sourceMappingURL=host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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"}
|