@procedures/peer 0.10.0 → 0.12.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 +78 -0
- package/README.md +46 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/host/faces.d.ts +78 -0
- package/dist/host/faces.d.ts.map +1 -0
- package/dist/host/faces.js +77 -0
- package/dist/host/faces.js.map +1 -0
- package/dist/host/index.d.ts +186 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +257 -0
- package/dist/host/index.js.map +1 -0
- package/dist/host/tracking.d.ts +60 -0
- package/dist/host/tracking.d.ts.map +1 -0
- package/dist/host/tracking.js +162 -0
- package/dist/host/tracking.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/lockfile.d.ts +21 -4
- package/dist/lockfile.d.ts.map +1 -1
- package/dist/lockfile.js +31 -7
- package/dist/lockfile.js.map +1 -1
- package/dist/peer/index.d.ts +43 -9
- package/dist/peer/index.d.ts.map +1 -1
- package/dist/peer/index.js +156 -241
- package/dist/peer/index.js.map +1 -1
- package/package.json +6 -9
- package/src/host/faces.ts +130 -0
- package/src/host/index.ts +434 -0
- package/src/host/tracking.ts +217 -0
- package/src/index.ts +38 -2
- package/src/lockfile.ts +39 -7
- package/src/peer/index.ts +172 -292
package/src/peer/index.ts
CHANGED
|
@@ -1,49 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Peer Module
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* A peer is a procedures host with a service of its own (CLI-7), for a process that has no service
|
|
5
|
+
* of its own: a test, or a host that a procedure starts (`server.create`). `createPeer()` builds it
|
|
6
|
+
* with `createProcedureHost()`: each HTTP or WebSocket transport of the options is a face on a
|
|
7
|
+
* listener of the host, and the listener is an `httpTransport` of `@mark1russell7/service/http`.
|
|
8
|
+
*
|
|
9
|
+
* - `start()` declares the parts in a service and starts `run()` of `@mark1russell7/service`, on
|
|
10
|
+
* a process port that installs no signal handler and never ends the process.
|
|
11
|
+
* - `stop()` asks the lifecycle for its shutdown, and waits for the end of its stages.
|
|
12
|
+
*
|
|
13
|
+
* A process that has a service of its own declares the parts of `createProcedureHost()` with
|
|
14
|
+
* `host.declare(svc)`, in place of a peer (decision D2). The peer has no listener, URL strategy,
|
|
15
|
+
* `/health` route or token check of its own.
|
|
5
16
|
*/
|
|
6
17
|
|
|
18
|
+
import { daemon, embeddedProcess, service, type Lifecycle, type Profile } from "@mark1russell7/service";
|
|
7
19
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
WebSocketServerTransport,
|
|
11
|
-
type ProcedureRegistry,
|
|
12
|
-
PROCEDURE_REGISTRY,
|
|
13
|
-
rpcServerUrlStrategy,
|
|
14
|
-
defaultServerUrlStrategy,
|
|
15
|
-
defaultUrlPattern,
|
|
16
|
-
procedureServerOptions,
|
|
17
|
-
hostProcedures,
|
|
18
|
-
metricsInterceptor,
|
|
19
|
-
Client,
|
|
20
|
+
hostPolicy,
|
|
21
|
+
seconds,
|
|
20
22
|
type BrowserGuardOptions,
|
|
23
|
+
type Client,
|
|
21
24
|
type HostPolicy,
|
|
22
|
-
type
|
|
25
|
+
type ProcedurePath,
|
|
26
|
+
type ProcedureRegistry,
|
|
27
|
+
type ProcedureServer,
|
|
28
|
+
type StopOptions,
|
|
23
29
|
} from "@procedures/client";
|
|
24
|
-
import type
|
|
25
|
-
import type { ProcedurePath, StopOptions } from "@procedures/client";
|
|
30
|
+
import { defaultServerUrlStrategy, rpcServerUrlStrategy, type ProcedureHandlerOptions, type UpgradeHandlerOptions } from "@procedures/client/node";
|
|
26
31
|
import type { HttpTransportConfig, TransportConfig, TransportType, WebSocketTransportConfig } from "../types.js";
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* URL strategy that matches the client's defaultUrlPattern
|
|
31
|
-
* Maps: POST /api/server/status → { service: "server", operation: "status" }
|
|
32
|
-
*/
|
|
33
|
-
function patternUrlStrategy(req: Request): Method | null {
|
|
34
|
-
const basePath = "/api";
|
|
35
|
-
let path = req.path;
|
|
36
|
-
|
|
37
|
-
// Remove base path prefix if present
|
|
38
|
-
if (path.startsWith(basePath)) {
|
|
39
|
-
path = path.slice(basePath.length);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Use the client's defaultUrlPattern parser
|
|
43
|
-
// Cast method to expected type (parser doesn't actually use it for this pattern)
|
|
44
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
-
return defaultUrlPattern.parse(path, req.method.toUpperCase() as any);
|
|
46
|
-
}
|
|
32
|
+
import { createProcedureHost, httpFace, LOCAL_LISTENER_KNOBS, wsFace, type HostFaceFactory, type HostListener, type ProcedureHost, type ProcedureHostOptions } from "../host/index.js";
|
|
33
|
+
import { originAllowed } from "./security.js";
|
|
47
34
|
|
|
48
35
|
// =============================================================================
|
|
49
36
|
// Types
|
|
@@ -67,16 +54,13 @@ export interface PeerOptions {
|
|
|
67
54
|
metrics?: boolean | undefined;
|
|
68
55
|
/**
|
|
69
56
|
* The policy of the host (`hostPolicy()` of the core, roadmap 2.3): the expose rule, the call
|
|
70
|
-
* rule, the interceptors (a time limit), the token
|
|
71
|
-
*
|
|
57
|
+
* rule, the interceptors (a time limit), the token and the browser checks. `expose` above, when
|
|
58
|
+
* given, wins over the policy's. A peer has one token: the token of its transports, or else the
|
|
59
|
+
* token of the policy.
|
|
72
60
|
*/
|
|
73
61
|
policy?: HostPolicy | undefined;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/** The port that a listening server got, or undefined. With port 0 the system gives a free port. */
|
|
77
|
-
function listeningPort(server: import("node:http").Server): number | undefined {
|
|
78
|
-
const address = server.address();
|
|
79
|
-
return typeof address === "object" && address !== null ? address.port : undefined;
|
|
62
|
+
/** The profile of the service of the peer. The default is `PEER_PROFILE`. */
|
|
63
|
+
profile?: Profile | undefined;
|
|
80
64
|
}
|
|
81
65
|
|
|
82
66
|
export interface PeerEndpoint {
|
|
@@ -87,15 +71,21 @@ export interface PeerEndpoint {
|
|
|
87
71
|
export interface Peer {
|
|
88
72
|
/** Peer ID */
|
|
89
73
|
id: string;
|
|
90
|
-
/**
|
|
74
|
+
/** This method starts the parts of the host. A peer starts one time. */
|
|
91
75
|
start(): Promise<void>;
|
|
92
|
-
/**
|
|
93
|
-
|
|
76
|
+
/**
|
|
77
|
+
* This method asks the lifecycle of the peer for its shutdown, and waits for the end of its stages.
|
|
78
|
+
*
|
|
79
|
+
* @param options - Deprecated: the grace has no effect. The drain of the profile of the peer gives
|
|
80
|
+
* the calls in flight their time (CLI-7).
|
|
81
|
+
*/
|
|
94
82
|
stop(options?: StopOptions): Promise<void>;
|
|
95
83
|
/** Get active endpoints */
|
|
96
84
|
getEndpoints(): PeerEndpoint[];
|
|
97
85
|
/** Get underlying server */
|
|
98
86
|
getServer(): ProcedureServer;
|
|
87
|
+
/** The procedures host of the peer: its parts, its faces and its info (CLI-7). */
|
|
88
|
+
getHost(): ProcedureHost;
|
|
99
89
|
/** The ids of the WebSocket connections of the clients (roadmap R24). */
|
|
100
90
|
connections(): string[];
|
|
101
91
|
/**
|
|
@@ -109,277 +99,166 @@ export interface Peer {
|
|
|
109
99
|
// Implementation
|
|
110
100
|
// =============================================================================
|
|
111
101
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
private ownedServers: Array<import("node:http").Server> = [];
|
|
122
|
-
private registry: ProcedureRegistry;
|
|
123
|
-
private startTime: number = Date.now();
|
|
124
|
-
|
|
125
|
-
/** The browser checks of the policy (an origin rule here replaces the peer's own rule). */
|
|
126
|
-
private browser: BrowserGuardOptions;
|
|
127
|
-
|
|
128
|
-
constructor(options: PeerOptions) {
|
|
129
|
-
this.id = options.id;
|
|
130
|
-
const policy = options.policy;
|
|
131
|
-
// The policy's token goes to each network transport that has none of its own
|
|
132
|
-
this.transports = options.transports.map((transport) =>
|
|
133
|
-
transport.type === "local" || policy?.token === undefined || transport.token !== undefined
|
|
134
|
-
? transport
|
|
135
|
-
: { ...transport, token: policy.token }
|
|
136
|
-
);
|
|
137
|
-
this.registry = options.registry ?? PROCEDURE_REGISTRY;
|
|
138
|
-
this.browser = policy?.browser ?? {};
|
|
139
|
-
|
|
140
|
-
// Create procedure server. The metrics interceptor counts each call for host.metrics (R19).
|
|
141
|
-
const metrics = options.metrics === false ? undefined : metricsInterceptor();
|
|
142
|
-
const serverOptions = policy ? procedureServerOptions(policy, this.registry) : {};
|
|
143
|
-
const interceptors = [...(serverOptions.interceptors ?? []), ...(metrics ? [metrics.interceptor] : [])];
|
|
144
|
-
this.server = new ProcedureServer({
|
|
145
|
-
...serverOptions,
|
|
146
|
-
...(interceptors.length > 0 ? { interceptors } : {}),
|
|
147
|
-
autoRegister: options.autoRegister ?? true,
|
|
148
|
-
registry: this.registry,
|
|
149
|
-
...(options.expose ? { expose: options.expose } : {}),
|
|
150
|
-
});
|
|
151
|
-
// host.health, host.info and host.metrics: pinned on the server, not in the registry (R19)
|
|
152
|
-
this.server.registerProcedures(
|
|
153
|
-
hostProcedures({
|
|
154
|
-
id: this.id,
|
|
155
|
-
startedAt: this.startTime,
|
|
156
|
-
server: this.server,
|
|
157
|
-
endpoints: () => this.getEndpoints(),
|
|
158
|
-
...(metrics ? { metrics } : {}),
|
|
159
|
-
})
|
|
160
|
-
);
|
|
102
|
+
/**
|
|
103
|
+
* The policy of a peer, with its one token. A transport token and a policy token must be the same:
|
|
104
|
+
* the one server examines one token for each face.
|
|
105
|
+
*/
|
|
106
|
+
function peerPolicy(options: PeerOptions): HostPolicy {
|
|
107
|
+
const own = new Set(options.transports.flatMap((transport) => (transport.type !== "local" && transport.token !== undefined ? [transport.token] : [])));
|
|
108
|
+
const policyToken = options.policy?.token;
|
|
109
|
+
if (own.size > 1 || (own.size === 1 && policyToken !== undefined && !own.has(policyToken))) {
|
|
110
|
+
throw new Error("A peer has one token: give the same token to each transport, or give it only in the policy");
|
|
161
111
|
}
|
|
112
|
+
const token = [...own][0] ?? policyToken;
|
|
113
|
+
if (options.policy) return token === policyToken ? options.policy : { ...options.policy, token };
|
|
114
|
+
return hostPolicy(token !== undefined ? { token } : {});
|
|
115
|
+
}
|
|
162
116
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
117
|
+
/** The options of the HTTP face of a transport: the rules of the peer before CLI-7, on `procedureHandler`. */
|
|
118
|
+
function httpOptionsOf(config: HttpTransportConfig, browser: BrowserGuardOptions): ProcedureHandlerOptions {
|
|
119
|
+
return {
|
|
120
|
+
basePath: config.basePath ?? "/api",
|
|
121
|
+
// Off by default: before, every peer sent Access-Control-Allow-Origin: * (deep dive CLI-2)
|
|
122
|
+
cors: config.cors ?? false,
|
|
123
|
+
...(config.urlStrategy === "rpc" ? { urlStrategy: rpcServerUrlStrategy } : config.urlStrategy === "rest" ? { urlStrategy: defaultServerUrlStrategy } : {}),
|
|
124
|
+
// The browser checks follow the rule of the peer: loopback origins and the listed ones
|
|
125
|
+
allowedOrigins: browser.allowedOrigins ?? ((origin: string) => originAllowed(origin, config.corsOrigins)),
|
|
126
|
+
...(browser.allowedHosts !== undefined ? { allowedHosts: browser.allowedHosts } : {}),
|
|
127
|
+
// The RESTful strategy calls by GET
|
|
128
|
+
allowGet: browser.allowGet ?? config.urlStrategy === "rest",
|
|
129
|
+
...(config.bodyLimit !== undefined ? { bodyLimit: config.bodyLimit } : {}),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
176
132
|
|
|
177
|
-
|
|
178
|
-
|
|
133
|
+
/** The options of the WebSocket face of a transport. */
|
|
134
|
+
function wsOptionsOf(config: WebSocketTransportConfig, browser: BrowserGuardOptions): UpgradeHandlerOptions {
|
|
135
|
+
return {
|
|
136
|
+
path: config.path ?? "/ws",
|
|
137
|
+
allowedOrigins: browser.allowedOrigins ?? ((origin: string) => originAllowed(origin, config.origins)),
|
|
138
|
+
...(browser.allowedHosts !== undefined ? { allowedHosts: browser.allowedHosts } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
179
141
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
142
|
+
/**
|
|
143
|
+
* The faces and the listeners of the transports. Two transports on one address and port share one
|
|
144
|
+
* listener. Loopback is the default address, so the whole registry (`shell.run`, `fs.write`) is
|
|
145
|
+
* not served on each interface (BUGS-2026-07 H19). Give `host: "0.0.0.0"` on purpose for the LAN.
|
|
146
|
+
*/
|
|
147
|
+
function facesOf(transports: readonly TransportConfig[], browser: BrowserGuardOptions): { faces: Record<string, HostFaceFactory>; listen: HostListener[] } {
|
|
148
|
+
const faces: Record<string, HostFaceFactory> = {};
|
|
149
|
+
const listeners: Array<{ name: string; port: number; host: string; faces: string[] }> = [];
|
|
150
|
+
const counts = { http: 0, websocket: 0 };
|
|
151
|
+
for (const transport of transports) {
|
|
152
|
+
if (transport.type === "local") continue;
|
|
153
|
+
const kind = transport.type === "http" ? "http" : "ws";
|
|
154
|
+
const index = counts[transport.type]++;
|
|
155
|
+
const faceName = index === 0 ? kind : `${kind}${index + 1}`;
|
|
156
|
+
faces[faceName] =
|
|
157
|
+
transport.type === "http" ? httpFace(httpOptionsOf(transport, browser)) : wsFace(wsOptionsOf(transport, browser));
|
|
158
|
+
const port = transport.port ?? (transport.type === "http" ? 3000 : 3001);
|
|
159
|
+
const host = transport.host ?? "127.0.0.1";
|
|
160
|
+
const shared = port === 0 ? undefined : listeners.find((listener) => listener.port === port && listener.host === host);
|
|
161
|
+
if (shared) shared.faces.push(faceName);
|
|
162
|
+
else listeners.push({ name: faceName, port, host, faces: [faceName] });
|
|
196
163
|
}
|
|
164
|
+
return { faces, listen: listeners.map((listener) => ({ ...listener, knobs: LOCAL_LISTENER_KNOBS })) };
|
|
165
|
+
}
|
|
197
166
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
const isHealth = req.path === "/health" || req.path === `${basePath}/health`;
|
|
222
|
-
if (!isHealth && req.method !== "OPTIONS" && !tokenMatches(config.token, bearerToken(req.headers.authorization))) {
|
|
223
|
-
res.status(401).json({ error: "Unauthorized: this server requires its token", code: "UNAUTHORIZED" });
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
next();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// Health endpoint for container orchestration
|
|
230
|
-
app.get(`${basePath}/health`, (_req, res) => {
|
|
231
|
-
const procedureCount = this.registry.getAll().filter((p) => p.handler).length;
|
|
232
|
-
res.json({
|
|
233
|
-
status: "ok",
|
|
234
|
-
uptime: Date.now() - this.startTime,
|
|
235
|
-
procedures: procedureCount,
|
|
236
|
-
peerId: this.id,
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// Also add at root /health for convenience
|
|
241
|
-
app.get("/health", (_req, res) => {
|
|
242
|
-
const procedureCount = this.registry.getAll().filter((p) => p.handler).length;
|
|
243
|
-
res.json({
|
|
244
|
-
status: "ok",
|
|
245
|
-
uptime: Date.now() - this.startTime,
|
|
246
|
-
procedures: procedureCount,
|
|
247
|
-
peerId: this.id,
|
|
248
|
-
});
|
|
249
|
-
});
|
|
167
|
+
/**
|
|
168
|
+
* This function gives the options of a procedures host for the options of a peer: each HTTP or
|
|
169
|
+
* WebSocket transport is a face on a listener with `LOCAL_LISTENER_KNOBS`, and the peer has one
|
|
170
|
+
* token. `createPeer()` uses it, and so does `runProcedureHost()` of `@procedures/server` (CLI-8).
|
|
171
|
+
*/
|
|
172
|
+
export function peerHostOptions(options: PeerOptions, name: string = "peer"): ProcedureHostOptions {
|
|
173
|
+
const policy = peerPolicy(options);
|
|
174
|
+
const { faces, listen } = facesOf(options.transports, policy.browser);
|
|
175
|
+
const local = options.transports.some((transport) => transport.type === "local");
|
|
176
|
+
return {
|
|
177
|
+
name,
|
|
178
|
+
id: options.id,
|
|
179
|
+
policy,
|
|
180
|
+
faces,
|
|
181
|
+
listen,
|
|
182
|
+
autoRegister: options.autoRegister ?? true,
|
|
183
|
+
...(options.registry ? { registry: options.registry } : {}),
|
|
184
|
+
...(options.expose ? { expose: options.expose } : {}),
|
|
185
|
+
...(options.metrics !== undefined ? { metrics: options.metrics } : {}),
|
|
186
|
+
...(local ? { endpoints: [{ type: "local", address: "local://in-process" }] } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
250
189
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const urlStrategy = config.urlStrategy === "rpc"
|
|
254
|
-
? rpcServerUrlStrategy
|
|
255
|
-
: config.urlStrategy === "rest"
|
|
256
|
-
? defaultServerUrlStrategy
|
|
257
|
-
: patternUrlStrategy;
|
|
190
|
+
/** The cap of the stage `drain` of the profile `daemon`. */
|
|
191
|
+
const DAEMON_DRAIN = daemon.phases?.find((phase) => phase.name === "drain")?.cap ?? seconds(4);
|
|
258
192
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
cors: config.cors ?? false,
|
|
266
|
-
urlStrategy,
|
|
267
|
-
// The transport's browser checks follow the peer's rule: loopback origins and the listed
|
|
268
|
-
// ones (the token is required in any case). The RESTful strategy calls by GET.
|
|
269
|
-
allowedOrigins: this.browser.allowedOrigins ?? ((origin) => originAllowed(origin, config.corsOrigins)),
|
|
270
|
-
...(this.browser.allowedHosts ? { allowedHosts: this.browser.allowedHosts } : {}),
|
|
271
|
-
allowGet: this.browser.allowGet ?? config.urlStrategy === "rest",
|
|
272
|
-
...(config.bodyLimit !== undefined ? { bodyLimit: config.bodyLimit } : {}),
|
|
273
|
-
});
|
|
193
|
+
/**
|
|
194
|
+
* The profile of the service of a peer, as data: the caps of `daemon`, and the cooperative abort at
|
|
195
|
+
* the start of the drain. Thus `stop()` aborts the calls in flight at once, as `stop()` without a
|
|
196
|
+
* grace did before (CLI-7). A repeat signal has no effect, and the port of a peer has no signal.
|
|
197
|
+
*/
|
|
198
|
+
export const PEER_PROFILE: Profile = Object.freeze({ ...daemon, cooperativeWindow: DAEMON_DRAIN, cooperativeFloor: "none" as const });
|
|
274
199
|
|
|
275
|
-
|
|
276
|
-
|
|
200
|
+
class PeerImpl implements Peer {
|
|
201
|
+
readonly id: string;
|
|
202
|
+
private readonly host: ProcedureHost;
|
|
203
|
+
private readonly profile: Profile;
|
|
204
|
+
private life: Lifecycle | undefined;
|
|
205
|
+
private state: "new" | "started" | "stopped" = "new";
|
|
277
206
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
});
|
|
207
|
+
constructor(options: PeerOptions) {
|
|
208
|
+
this.id = options.id;
|
|
209
|
+
this.host = createProcedureHost(peerHostOptions(options));
|
|
210
|
+
this.profile = options.profile ?? PEER_PROFILE;
|
|
283
211
|
}
|
|
284
212
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const onError = (error: Error): void => {
|
|
304
|
-
reject(new Error(`The WebSocket server cannot listen on ${host}:${port}: ${error.message}`));
|
|
305
|
-
};
|
|
306
|
-
httpServer.once("error", onError);
|
|
307
|
-
httpServer.listen(port, host, () => {
|
|
308
|
-
httpServer.off("error", onError);
|
|
309
|
-
resolve();
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
const wsTransport = new WebSocketServerTransport(this.server, {
|
|
314
|
-
server: httpServer,
|
|
315
|
-
path,
|
|
316
|
-
allowedOrigins: this.browser.allowedOrigins ?? ((origin) => originAllowed(origin, config.origins)),
|
|
317
|
-
...(this.browser.allowedHosts ? { allowedHosts: this.browser.allowedHosts } : {}),
|
|
318
|
-
// Host, origin and token checks: before, any web page could open a connection and call
|
|
319
|
-
// procedures (cross-site WebSocket hijacking, deep dive TRN-1)
|
|
320
|
-
authenticate: (req) => {
|
|
321
|
-
const token = new URL(req.url ?? "/", "http://localhost").searchParams.get("token") ?? bearerToken(req.headers.authorization);
|
|
322
|
-
return (
|
|
323
|
-
hostAllowed(host, req.headers.host) &&
|
|
324
|
-
originAllowed(req.headers.origin, config.origins) &&
|
|
325
|
-
tokenMatches(config.token, token)
|
|
326
|
-
);
|
|
327
|
-
},
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
this.server.addTransport(wsTransport);
|
|
331
|
-
try {
|
|
332
|
-
await wsTransport.start();
|
|
333
|
-
} catch (error) {
|
|
334
|
-
httpServer.close();
|
|
335
|
-
throw error;
|
|
213
|
+
async start(): Promise<void> {
|
|
214
|
+
if (this.state === "started") return;
|
|
215
|
+
if (this.state === "stopped") throw new Error("This peer stopped. A peer starts one time: create a new peer.");
|
|
216
|
+
this.state = "started";
|
|
217
|
+
const svc = this.host.declare(service(this.id).profile(this.profile));
|
|
218
|
+
// No check gates the policies of a peer: it is ready from the end of its startup
|
|
219
|
+
if (!this.profile.readiness) svc.readiness({ type: "debounce", interval: seconds(5), failAfter: 3, recoverAfter: 1 });
|
|
220
|
+
if (!this.profile.liveness) svc.liveness({ type: "window", failAfter: seconds(45) });
|
|
221
|
+
if (!this.profile.startup) svc.startup(seconds(30));
|
|
222
|
+
if (!this.profile.onRepeatSignal) svc.onRepeatSignal("ignore");
|
|
223
|
+
// The port of a lifecycle in a process that another part owns (SYS-27): no signal handler, no
|
|
224
|
+
// hold of the process, and an exit that only records the code
|
|
225
|
+
const life = await svc.run({ process: embeddedProcess() });
|
|
226
|
+
if (life.state !== "running") {
|
|
227
|
+
// The startup failed: the lifecycle stops the parts that started, so no port stays open
|
|
228
|
+
this.state = "stopped";
|
|
229
|
+
await life.done;
|
|
230
|
+
throw new Error(life.errors.join("; ") || "The peer did not start");
|
|
336
231
|
}
|
|
337
|
-
this.
|
|
338
|
-
this.wsTransports.push(wsTransport);
|
|
339
|
-
|
|
340
|
-
this.endpoints.push({
|
|
341
|
-
type: "websocket",
|
|
342
|
-
address: `ws://${host === "0.0.0.0" ? "localhost" : host}:${listeningPort(httpServer) ?? port}${path}`,
|
|
343
|
-
});
|
|
232
|
+
this.life = life;
|
|
344
233
|
}
|
|
345
234
|
|
|
346
|
-
async stop(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
this.ownedServers = [];
|
|
353
|
-
await Promise.all(
|
|
354
|
-
owned.map(
|
|
355
|
-
(httpServer) =>
|
|
356
|
-
new Promise<void>((resolve) => {
|
|
357
|
-
httpServer.close(() => resolve());
|
|
358
|
-
httpServer.closeAllConnections();
|
|
359
|
-
}),
|
|
360
|
-
),
|
|
361
|
-
);
|
|
362
|
-
this.started = false;
|
|
363
|
-
this.endpoints = [];
|
|
364
|
-
this.wsTransports = [];
|
|
235
|
+
async stop(_options: StopOptions = {}): Promise<void> {
|
|
236
|
+
const life = this.life;
|
|
237
|
+
if (this.state !== "started" || life === undefined) return;
|
|
238
|
+
this.state = "stopped";
|
|
239
|
+
await life.shutdown("the peer stops");
|
|
240
|
+
await life.done;
|
|
365
241
|
}
|
|
366
242
|
|
|
367
243
|
getEndpoints(): PeerEndpoint[] {
|
|
368
|
-
return [
|
|
244
|
+
if (this.state !== "started" || this.life === undefined) return [];
|
|
245
|
+
return this.host.info.endpoints().map((endpoint) => ({ type: endpoint.type as TransportType, address: endpoint.address }));
|
|
369
246
|
}
|
|
370
247
|
|
|
371
248
|
getServer(): ProcedureServer {
|
|
372
|
-
return this.server;
|
|
249
|
+
return this.host.server;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
getHost(): ProcedureHost {
|
|
253
|
+
return this.host;
|
|
373
254
|
}
|
|
374
255
|
|
|
375
256
|
connections(): string[] {
|
|
376
|
-
return this.
|
|
257
|
+
return this.host.connections();
|
|
377
258
|
}
|
|
378
259
|
|
|
379
260
|
clientFor(connectionId: string): Client {
|
|
380
|
-
|
|
381
|
-
if (!transport) throw new Error(`Connection not found: ${connectionId}`);
|
|
382
|
-
return new Client({ transport: transport.transportFor(connectionId), throwOnError: true });
|
|
261
|
+
return this.host.clientFor(connectionId);
|
|
383
262
|
}
|
|
384
263
|
}
|
|
385
264
|
|
|
@@ -388,7 +267,8 @@ class PeerImpl implements Peer {
|
|
|
388
267
|
// =============================================================================
|
|
389
268
|
|
|
390
269
|
/**
|
|
391
|
-
* Create a new peer instance
|
|
270
|
+
* Create a new peer instance: a procedures host with a service of its own, for a process that has
|
|
271
|
+
* no service of its own. A process with a service uses `createProcedureHost()` and `host.declare(svc)`.
|
|
392
272
|
*/
|
|
393
273
|
export async function createPeer(options: PeerOptions): Promise<Peer> {
|
|
394
274
|
return new PeerImpl(options);
|