@procedures/mark 0.23.0 → 0.24.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/CHANGELOG.md +55 -0
- package/README.md +49 -12
- package/dist/.tsbuildinfo +1 -1
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +4 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts +37 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +157 -78
- package/dist/cli.js.map +1 -1
- package/dist/client-mode.d.ts +26 -4
- package/dist/client-mode.d.ts.map +1 -1
- package/dist/client-mode.js +54 -33
- package/dist/client-mode.js.map +1 -1
- package/dist/command.d.ts +115 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +110 -0
- package/dist/command.js.map +1 -0
- package/dist/manifest.json +2 -2
- package/dist/server-mode.d.ts +2 -1
- package/dist/server-mode.d.ts.map +1 -1
- package/dist/server-mode.js +5 -4
- package/dist/server-mode.js.map +1 -1
- package/package.json +50 -49
- package/src/bin.ts +4 -1
- package/src/cli.ts +183 -79
- package/src/client-mode.ts +73 -38
- package/src/command.ts +181 -0
- package/src/server-mode.ts +5 -4
package/src/command.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The service of one `mark` command (CLI-26, decision D17).
|
|
3
|
+
*
|
|
4
|
+
* Each command that runs a procedure starts under `run()` of `@mark1russell7/service`, with the
|
|
5
|
+
* profile `ephemeral`. The runner owns the signals, the stop in stages and the exit. Thus this
|
|
6
|
+
* file installs no signal handler and calls no `process.exit`.
|
|
7
|
+
*
|
|
8
|
+
* - The warm mode: the CLI server (`mark --server`) runs the procedure. The service of the command
|
|
9
|
+
* has only the command, and the call goes to the server over HTTP.
|
|
10
|
+
* - The cold mode: the procedures host of the CLI server runs in this process, with the same
|
|
11
|
+
* registry and policy (`markHostOptions()`). It has no listener, no port and no lockfile. The
|
|
12
|
+
* call goes to the host through its local face (`localFace()` of `@procedures/peer`).
|
|
13
|
+
* - The command is one request of the controller `command`. A first Ctrl-C starts the stop. The
|
|
14
|
+
* drain aborts the signal of the request at once, and the call gets the abort.
|
|
15
|
+
* - After the command, `end()` asks for the stop with the exit code of the command. The process
|
|
16
|
+
* port then ends the process with that code.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { embeddedProcess, ephemeral, nodeProcess, service, type Lifecycle, type Profile, type RequestEnd, type RunOptions, type ServiceBuilder } from "@mark1russell7/service";
|
|
20
|
+
import type { Client, ProcedureRegistry } from "@procedures/client";
|
|
21
|
+
import type { ProcedureHost, ProcedureHostOptions, TransportConfig } from "@procedures/peer";
|
|
22
|
+
|
|
23
|
+
/** The name of the service of a command, and of the procedures host of the cold mode. */
|
|
24
|
+
export const COMMAND_SERVICE = "mark";
|
|
25
|
+
|
|
26
|
+
/** The controller of the one request of a command. */
|
|
27
|
+
export const COMMAND_CONTROLLER = "command";
|
|
28
|
+
|
|
29
|
+
/** The route of the one request of a command. A command can have effects, thus it is not idempotent. */
|
|
30
|
+
export const COMMAND_ROUTE = "RUN";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The profile of each command, in the warm mode and in the cold mode: `ephemeral`. The exit code is
|
|
34
|
+
* 130 after a first Ctrl-C, 143 after SIGTERM and 131 after SIGQUIT. A second signal during the
|
|
35
|
+
* stop ends the process at once with 1.
|
|
36
|
+
*
|
|
37
|
+
* Before CLI-26, a cold command gave 1 after a first Ctrl-C and 130 after a second one. A signal
|
|
38
|
+
* ended a warm command at once, and the server continued the call.
|
|
39
|
+
*/
|
|
40
|
+
export const COMMAND_PROFILE: Profile = ephemeral;
|
|
41
|
+
|
|
42
|
+
/** This function declares the controller and the route of the one request of a command. */
|
|
43
|
+
export function declareCommand(svc: ServiceBuilder): ServiceBuilder {
|
|
44
|
+
svc.controller(COMMAND_CONTROLLER).route(COMMAND_ROUTE, { idempotent: false });
|
|
45
|
+
return svc;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Where a command runs: the options of `run()`, and if the run owns its process. */
|
|
49
|
+
export interface CommandProcess {
|
|
50
|
+
/** The options of `run()`: the process port and the clock. A port here wins over `ownsProcess`. */
|
|
51
|
+
readonly run?: RunOptions | undefined;
|
|
52
|
+
/** True when the run owns its process, as the bin of `mark` does. */
|
|
53
|
+
readonly ownsProcess?: boolean | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* This function gives the options of `run()` for one command. A run that owns its process gets
|
|
58
|
+
* `nodeProcess`: the runner owns the signals, and it ends the process. Each other run gets a new
|
|
59
|
+
* `embeddedProcess()`: no signal handler, and no end of the process.
|
|
60
|
+
*/
|
|
61
|
+
export function commandRunOptions(given: CommandProcess = {}): RunOptions {
|
|
62
|
+
if (given.run?.process !== undefined) return given.run;
|
|
63
|
+
return { ...given.run, process: given.ownsProcess === true ? nodeProcess : embeddedProcess() };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A command that runs under `run()`. */
|
|
67
|
+
export interface CommandRun {
|
|
68
|
+
/** The lifecycle of the service of the command. */
|
|
69
|
+
readonly life: Lifecycle;
|
|
70
|
+
/**
|
|
71
|
+
* This method does the work of the command as the one request of the service. The signal of
|
|
72
|
+
* the work aborts at the start of the drain. The method gives the end of the request: `done`,
|
|
73
|
+
* `failed`, `refused` (the stop started before the work) or `cut` (the stop aborted the work).
|
|
74
|
+
*/
|
|
75
|
+
request(work: (signal: AbortSignal) => Promise<void>): Promise<RequestEnd>;
|
|
76
|
+
/**
|
|
77
|
+
* This method asks for the stop with the exit code of the command, and gives the exit code of
|
|
78
|
+
* the lifecycle. A signal that started the stop first keeps its exit code. With the process of
|
|
79
|
+
* Node, the process ends, and the promise does not resolve.
|
|
80
|
+
*/
|
|
81
|
+
end(code: number): Promise<number>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** This function gives the command of a lifecycle that `declareCommand()` declared. */
|
|
85
|
+
export function commandOf(life: Lifecycle): CommandRun {
|
|
86
|
+
return {
|
|
87
|
+
life,
|
|
88
|
+
async request(work) {
|
|
89
|
+
const stream = life.request<true>(COMMAND_CONTROLLER, COMMAND_ROUTE, async (signal) => {
|
|
90
|
+
await work(signal);
|
|
91
|
+
return true as const;
|
|
92
|
+
});
|
|
93
|
+
return (await stream.collect()).end;
|
|
94
|
+
},
|
|
95
|
+
async end(code) {
|
|
96
|
+
await life.shutdown("the command ended", { code });
|
|
97
|
+
return life.done;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** This function starts the service of a command on the CLI server (the warm mode). */
|
|
103
|
+
export async function startWarmCommand(where: CommandProcess = {}): Promise<CommandRun> {
|
|
104
|
+
const svc = declareCommand(service(COMMAND_SERVICE).profile(COMMAND_PROFILE));
|
|
105
|
+
return commandOf(await svc.run(commandRunOptions(where)));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The options of `markHostOptions()`. */
|
|
109
|
+
export interface MarkHostConfig {
|
|
110
|
+
/** The id of the host: `host.info` gives it as `peerId`. */
|
|
111
|
+
readonly id: string;
|
|
112
|
+
/** The registry of `mark` (`loadEcosystemRegistry()`). */
|
|
113
|
+
readonly registry: ProcedureRegistry;
|
|
114
|
+
/** The transports of the CLI server. The cold mode gives only the transport `local`. */
|
|
115
|
+
readonly transports: readonly TransportConfig[];
|
|
116
|
+
/** The token of the CLI server. The cold mode has none: each call of the local face is a call of this process. */
|
|
117
|
+
readonly token?: string | undefined;
|
|
118
|
+
/** The topic patterns of the remote events (`--events`). The default is none. */
|
|
119
|
+
readonly events?: readonly string[] | undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* This function gives the options of the procedures host of `mark`, for the CLI server and for the
|
|
124
|
+
* cold mode. Thus the two hosts serve the same procedures, with the same policy: `hostPolicy()` of
|
|
125
|
+
* the core, no expose list, and `events.subscribe` with the topics of `--events`.
|
|
126
|
+
*/
|
|
127
|
+
export async function markHostOptions(config: MarkHostConfig): Promise<ProcedureHostOptions> {
|
|
128
|
+
const { hostPolicy } = await import("@procedures/client");
|
|
129
|
+
const { peerHostOptions } = await import("@procedures/peer");
|
|
130
|
+
const policy = hostPolicy({ ...(config.token !== undefined ? { token: config.token } : {}), events: { topics: [...(config.events ?? [])] } });
|
|
131
|
+
return peerHostOptions({ id: config.id, transports: [...config.transports], registry: config.registry, policy }, "procedures");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A command in this process: its service, the procedures host and a client of the host. */
|
|
135
|
+
export interface ColdCommand {
|
|
136
|
+
readonly command: CommandRun;
|
|
137
|
+
readonly host: ProcedureHost;
|
|
138
|
+
/**
|
|
139
|
+
* A client of the host. `stream()` and `call()` go through the local face of the host. `exec()`
|
|
140
|
+
* runs a procedure reference with the registry of the host, as before (its nested refs need the rules of the registry).
|
|
141
|
+
*/
|
|
142
|
+
readonly client: Client;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The startup of a cold command that failed. The lifecycle stops the parts that started, and then
|
|
147
|
+
* it exits with the code 1: `life.done` gives the code. With the process of Node, the process ends.
|
|
148
|
+
*/
|
|
149
|
+
export interface ColdStartFailure {
|
|
150
|
+
readonly errors: readonly string[];
|
|
151
|
+
readonly life: Lifecycle;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* This function starts a command in this process (the cold mode): the procedures host of the CLI
|
|
156
|
+
* server, under `runProcedureHost()` with `COMMAND_PROFILE`. After a startup that failed, it
|
|
157
|
+
* gives the errors and the lifecycle.
|
|
158
|
+
*/
|
|
159
|
+
export async function startColdCommand(registry: ProcedureRegistry, where: CommandProcess = {}): Promise<ColdCommand | ColdStartFailure> {
|
|
160
|
+
const { Client } = await import("@procedures/client");
|
|
161
|
+
const { localFace } = await import("@procedures/peer");
|
|
162
|
+
const { runProcedureHost } = await import("@procedures/server");
|
|
163
|
+
const local = localFace();
|
|
164
|
+
const id = `cold-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
165
|
+
const options = await markHostOptions({ id, registry, transports: [{ type: "local" }] });
|
|
166
|
+
const running = await runProcedureHost({
|
|
167
|
+
name: COMMAND_SERVICE,
|
|
168
|
+
mode: "foreground",
|
|
169
|
+
profile: COMMAND_PROFILE,
|
|
170
|
+
host: { ...options, faces: { local: local.face } },
|
|
171
|
+
declare: (svc) => {
|
|
172
|
+
declareCommand(svc);
|
|
173
|
+
},
|
|
174
|
+
run: commandRunOptions(where),
|
|
175
|
+
});
|
|
176
|
+
if (running.life.state !== "running") {
|
|
177
|
+
return { errors: [...running.life.errors], life: running.life };
|
|
178
|
+
}
|
|
179
|
+
const client = new Client({ transport: local.transport }).useRegistry(registry);
|
|
180
|
+
return { command: commandOf(running.life), host: running.host, client };
|
|
181
|
+
}
|
package/src/server-mode.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* The server is the background host of `mark` (decision D17): it runs under `run()` of
|
|
10
10
|
* `@mark1russell7/service` through `runProcedureHost()`, with the profile `daemon` (CLI-8). The
|
|
11
11
|
* runner owns the signals, the stop in stages and the exit. The lockfile is a dependency of the
|
|
12
|
-
* service: its connect writes it, and its close removes it after the listeners close.
|
|
12
|
+
* service: its connect writes it, and its close removes it after the listeners close. The cold
|
|
13
|
+
* mode of `mark` runs the same host in its own process (`markHostOptions()`, CLI-26).
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import type { ProcedureHostRunConfig } from "@procedures/server";
|
|
@@ -108,8 +109,8 @@ export async function startServerMode(options: ServerModeOptions, run?: Procedur
|
|
|
108
109
|
// serves events.subscribe (EVENTS.md): the topic rule is the --events patterns, else empty.
|
|
109
110
|
// The bus is the global bus, which is also the ctx.bus of the procedures.
|
|
110
111
|
const events = [...(options.events ?? [])];
|
|
111
|
-
|
|
112
|
-
const {
|
|
112
|
+
// The options of the host: the same registry and policy as the cold mode of mark (CLI-26)
|
|
113
|
+
const { markHostOptions } = await import("./command.js");
|
|
113
114
|
const { runProcedureHost } = await import("@procedures/server");
|
|
114
115
|
const id = `peer-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
115
116
|
|
|
@@ -120,7 +121,7 @@ export async function startServerMode(options: ServerModeOptions, run?: Procedur
|
|
|
120
121
|
const running = await runProcedureHost({
|
|
121
122
|
name: "mark-server",
|
|
122
123
|
mode: "background",
|
|
123
|
-
host:
|
|
124
|
+
host: await markHostOptions({ id, registry, transports, token, events }),
|
|
124
125
|
lockfile: {
|
|
125
126
|
pid: process.pid,
|
|
126
127
|
port,
|