@push.rocks/smartrust 1.5.0 → 1.6.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.rustbridge.d.ts +19 -8
- package/dist_ts/classes.rustbridge.js +307 -78
- package/dist_ts/classes.sockettransport.d.ts +16 -2
- package/dist_ts/classes.sockettransport.js +239 -61
- package/dist_ts/classes.stdiotransport.d.ts +15 -3
- package/dist_ts/classes.stdiotransport.js +268 -59
- package/dist_ts/interfaces/config.d.ts +8 -0
- package/dist_ts/interfaces/transport.d.ts +9 -0
- package/package.json +1 -1
- package/readme.md +35 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.rustbridge.ts +368 -83
- package/ts/classes.sockettransport.ts +312 -60
- package/ts/classes.stdiotransport.ts +362 -58
- package/ts/interfaces/config.ts +9 -0
- package/ts/interfaces/transport.ts +12 -0
package/ts/classes.rustbridge.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { SocketTransport } from './classes.sockettransport.js';
|
|
|
7
7
|
import type {
|
|
8
8
|
IRustBridgeOptions,
|
|
9
9
|
IRustBridgeRequestOptions,
|
|
10
|
+
IRustBridgeTerminateOptions,
|
|
10
11
|
IRustBridgeLogger,
|
|
11
12
|
ISocketConnectOptions,
|
|
12
13
|
TCommandMap,
|
|
@@ -16,6 +17,7 @@ import type {
|
|
|
16
17
|
TStreamingCommandKeys,
|
|
17
18
|
TExtractChunk,
|
|
18
19
|
IRustTransport,
|
|
20
|
+
IRustTransportDisconnectOptions,
|
|
19
21
|
} from './interfaces/index.js';
|
|
20
22
|
|
|
21
23
|
const defaultLogger: IRustBridgeLogger = {
|
|
@@ -36,6 +38,12 @@ type TPendingRequestSettlement =
|
|
|
36
38
|
| { outcome: 'resolve'; value: any }
|
|
37
39
|
| { outcome: 'reject'; error: Error };
|
|
38
40
|
|
|
41
|
+
type TRustBridgeLifecycleState = 'idle' | 'starting' | 'ready' | 'stopping';
|
|
42
|
+
|
|
43
|
+
type TConfirmableRustTransport = IRustTransport & {
|
|
44
|
+
disconnectAndWait(optionsArg?: IRustTransportDisconnectOptions): Promise<void>;
|
|
45
|
+
};
|
|
46
|
+
|
|
39
47
|
/**
|
|
40
48
|
* Generic bridge between TypeScript and a Rust binary.
|
|
41
49
|
* Communicates via JSON-over-stdin/stdout IPC protocol (stdio mode)
|
|
@@ -47,11 +55,15 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
47
55
|
private locator: RustBinaryLocator;
|
|
48
56
|
private options: Required<Pick<IRustBridgeOptions, 'cliArgs' | 'requestTimeoutMs' | 'readyTimeoutMs' | 'readyEventName' | 'maxPayloadSize'>> & IRustBridgeOptions;
|
|
49
57
|
private logger: IRustBridgeLogger;
|
|
50
|
-
private transport:
|
|
58
|
+
private transport: TConfirmableRustTransport | null = null;
|
|
51
59
|
private pendingRequests = new Map<string, IPendingRequest>();
|
|
52
60
|
private requestCounter = 0;
|
|
53
61
|
private isRunning = false;
|
|
54
62
|
private binaryPath: string | null = null;
|
|
63
|
+
private lifecycleState: TRustBridgeLifecycleState = 'idle';
|
|
64
|
+
private lifecycleGeneration = 0;
|
|
65
|
+
private activationPromise: Promise<boolean> | null = null;
|
|
66
|
+
private terminationPromise: Promise<void> | null = null;
|
|
55
67
|
|
|
56
68
|
constructor(options: IRustBridgeOptions) {
|
|
57
69
|
super();
|
|
@@ -71,21 +83,25 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
71
83
|
* Spawn the Rust binary and wait for it to signal readiness.
|
|
72
84
|
* Returns true if the binary was found and spawned successfully.
|
|
73
85
|
*/
|
|
74
|
-
public
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
public spawn(): Promise<boolean> {
|
|
87
|
+
return this.activate(async (generationArg) => {
|
|
88
|
+
const binaryPath = await this.locator.findBinary();
|
|
89
|
+
if (!this.isStartingGeneration(generationArg)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
this.binaryPath = binaryPath;
|
|
93
|
+
if (!binaryPath) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
79
96
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
97
|
+
return new StdioTransport({
|
|
98
|
+
binaryPath,
|
|
99
|
+
cliArgs: this.options.cliArgs,
|
|
100
|
+
env: this.options.env,
|
|
101
|
+
maxPayloadSize: this.options.maxPayloadSize,
|
|
102
|
+
logger: this.logger,
|
|
103
|
+
});
|
|
86
104
|
});
|
|
87
|
-
|
|
88
|
-
return this.connectWithTransport(transport);
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
/**
|
|
@@ -95,8 +111,8 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
95
111
|
* @param socketPath - Path to Unix socket or Windows named pipe
|
|
96
112
|
* @param socketOptions - Optional socket connection options (reconnect, etc.)
|
|
97
113
|
*/
|
|
98
|
-
public
|
|
99
|
-
|
|
114
|
+
public connect(socketPath: string, socketOptions?: ISocketConnectOptions): Promise<boolean> {
|
|
115
|
+
return this.activate(async () => new SocketTransport({
|
|
100
116
|
socketPath,
|
|
101
117
|
maxPayloadSize: this.options.maxPayloadSize,
|
|
102
118
|
logger: this.logger,
|
|
@@ -104,68 +120,252 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
104
120
|
reconnectBaseDelayMs: socketOptions?.reconnectBaseDelayMs,
|
|
105
121
|
reconnectMaxDelayMs: socketOptions?.reconnectMaxDelayMs,
|
|
106
122
|
maxReconnectAttempts: socketOptions?.maxReconnectAttempts,
|
|
107
|
-
});
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
108
125
|
|
|
109
|
-
|
|
126
|
+
/** Reserve activation synchronously, or sequence it after confirmed termination. */
|
|
127
|
+
private activate(
|
|
128
|
+
createTransportArg: (generationArg: number) => Promise<TConfirmableRustTransport | null>,
|
|
129
|
+
): Promise<boolean> {
|
|
130
|
+
if (this.lifecycleState === 'stopping' && this.terminationPromise) {
|
|
131
|
+
return this.terminationPromise.then(() => this.beginActivation(createTransportArg));
|
|
132
|
+
}
|
|
133
|
+
if (this.lifecycleState !== 'idle') {
|
|
134
|
+
return Promise.reject(
|
|
135
|
+
new Error(`RustBridge cannot activate while lifecycle state is '${this.lifecycleState}'`),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return this.beginActivation(createTransportArg);
|
|
110
139
|
}
|
|
111
140
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
141
|
+
/** Start one transport generation. A second concurrent activation is rejected. */
|
|
142
|
+
private beginActivation(
|
|
143
|
+
createTransportArg: (generationArg: number) => Promise<TConfirmableRustTransport | null>,
|
|
144
|
+
): Promise<boolean> {
|
|
145
|
+
if (this.lifecycleState !== 'idle') {
|
|
146
|
+
throw new Error(`RustBridge cannot activate while lifecycle state is '${this.lifecycleState}'`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const generation = ++this.lifecycleGeneration;
|
|
150
|
+
this.lifecycleState = 'starting';
|
|
151
|
+
|
|
152
|
+
const activationPromise = (async (): Promise<boolean> => {
|
|
117
153
|
try {
|
|
154
|
+
const transport = await createTransportArg(generation);
|
|
155
|
+
if (!transport || !this.isStartingGeneration(generation)) {
|
|
156
|
+
if (this.isStartingGeneration(generation)) {
|
|
157
|
+
this.lifecycleState = 'idle';
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
118
162
|
this.transport = transport;
|
|
163
|
+
return await this.connectWithTransport(transport, generation);
|
|
164
|
+
} catch (error: unknown) {
|
|
165
|
+
this.logger.log('error', `Failed to connect: ${this.getErrorMessage(error)}`);
|
|
166
|
+
if (this.isStartingGeneration(generation)) {
|
|
167
|
+
this.lifecycleState = 'idle';
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
})();
|
|
119
172
|
|
|
120
|
-
|
|
121
|
-
|
|
173
|
+
this.activationPromise = activationPromise;
|
|
174
|
+
void activationPromise.then(
|
|
175
|
+
() => {
|
|
176
|
+
if (this.activationPromise === activationPromise) {
|
|
177
|
+
this.activationPromise = null;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
() => {
|
|
181
|
+
if (this.activationPromise === activationPromise) {
|
|
182
|
+
this.activationPromise = null;
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
return activationPromise;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Wire one transport generation and wait for its ready handshake. */
|
|
190
|
+
private connectWithTransport(
|
|
191
|
+
transportArg: TConfirmableRustTransport,
|
|
192
|
+
generationArg?: number,
|
|
193
|
+
): Promise<boolean> {
|
|
194
|
+
const generation = generationArg ?? ++this.lifecycleGeneration;
|
|
195
|
+
if (generationArg === undefined) {
|
|
196
|
+
if (this.lifecycleState !== 'idle') {
|
|
197
|
+
throw new Error(`RustBridge cannot activate while lifecycle state is '${this.lifecycleState}'`);
|
|
198
|
+
}
|
|
199
|
+
this.lifecycleState = 'starting';
|
|
200
|
+
this.transport = transportArg;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return new Promise<boolean>((resolve) => {
|
|
204
|
+
let settled = false;
|
|
205
|
+
let readyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
206
|
+
let reconnectReadyTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
207
|
+
|
|
208
|
+
const isCurrent = (): boolean => this.isCurrentTransport(transportArg, generation);
|
|
209
|
+
const settle = (valueArg: boolean): void => {
|
|
210
|
+
if (readyTimeout) {
|
|
211
|
+
clearTimeout(readyTimeout);
|
|
212
|
+
readyTimeout = null;
|
|
213
|
+
}
|
|
214
|
+
if (reconnectReadyTimeout) {
|
|
215
|
+
clearTimeout(reconnectReadyTimeout);
|
|
216
|
+
reconnectReadyTimeout = null;
|
|
217
|
+
}
|
|
218
|
+
if (settled) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
settled = true;
|
|
222
|
+
resolve(valueArg);
|
|
223
|
+
};
|
|
122
224
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
225
|
+
const requestConfirmedCleanup = (): void => {
|
|
226
|
+
let termination: Promise<void>;
|
|
227
|
+
try {
|
|
228
|
+
termination = this.terminate();
|
|
229
|
+
} catch (error: unknown) {
|
|
230
|
+
this.logger.log('error', `Transport cleanup failed: ${this.getErrorMessage(error)}`);
|
|
231
|
+
settle(false);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
void termination.then(
|
|
235
|
+
() => settle(false),
|
|
236
|
+
(error: unknown) => {
|
|
237
|
+
this.logger.log('error', `Transport cleanup failed: ${this.getErrorMessage(error)}`);
|
|
238
|
+
settle(false);
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
transportArg.on('line', (line: string) => {
|
|
244
|
+
if (!isCurrent()) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this.handleLine(line, () => {
|
|
248
|
+
if (!isCurrent() || this.lifecycleState !== 'starting') {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (reconnectReadyTimeout) {
|
|
252
|
+
clearTimeout(reconnectReadyTimeout);
|
|
253
|
+
reconnectReadyTimeout = null;
|
|
254
|
+
}
|
|
255
|
+
this.lifecycleState = 'ready';
|
|
256
|
+
this.isRunning = true;
|
|
257
|
+
this.logger.log('info', `Bridge connected to ${this.options.binaryName}`);
|
|
258
|
+
this.emit('ready');
|
|
259
|
+
settle(true);
|
|
126
260
|
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
transportArg.on('stderr', (line: string) => {
|
|
264
|
+
if (!isCurrent()) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
this.logger.log('debug', `[${this.options.binaryName}] ${line}`);
|
|
268
|
+
this.emit('stderr', line);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
transportArg.on('close', (...args: any[]) => {
|
|
272
|
+
if (!isCurrent()) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const intentional = this.lifecycleState === 'stopping';
|
|
276
|
+
this.logger.log('info', 'Transport closed');
|
|
277
|
+
this.isRunning = false;
|
|
278
|
+
this.rejectAllPendingRequests(`${this.options.binaryName} process exited`);
|
|
279
|
+
if (!intentional && transportArg.reconnecting === true) {
|
|
280
|
+
if (reconnectReadyTimeout) {
|
|
281
|
+
clearTimeout(reconnectReadyTimeout);
|
|
282
|
+
reconnectReadyTimeout = null;
|
|
283
|
+
}
|
|
284
|
+
this.lifecycleState = 'starting';
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
127
287
|
|
|
128
|
-
transport
|
|
129
|
-
|
|
130
|
-
this.
|
|
288
|
+
this.transport = null;
|
|
289
|
+
if (!intentional) {
|
|
290
|
+
this.lifecycleState = 'idle';
|
|
131
291
|
this.emit('exit', ...args);
|
|
132
|
-
}
|
|
292
|
+
}
|
|
293
|
+
settle(false);
|
|
294
|
+
});
|
|
133
295
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
});
|
|
296
|
+
transportArg.on('error', (errorArg: Error) => {
|
|
297
|
+
if (!isCurrent()) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.logger.log('error', `Transport error: ${errorArg.message}`);
|
|
301
|
+
this.isRunning = false;
|
|
302
|
+
this.rejectAllPendingRequests(`${this.options.binaryName} process exited`, errorArg);
|
|
303
|
+
});
|
|
139
304
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
305
|
+
transportArg.on('reconnected', () => {
|
|
306
|
+
if (!isCurrent()) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
this.logger.log('info', 'Transport reconnected, waiting for ready event');
|
|
310
|
+
this.lifecycleState = 'starting';
|
|
311
|
+
if (reconnectReadyTimeout) {
|
|
312
|
+
clearTimeout(reconnectReadyTimeout);
|
|
313
|
+
}
|
|
314
|
+
reconnectReadyTimeout = setTimeout(() => {
|
|
315
|
+
if (!isCurrent() || this.lifecycleState !== 'starting') {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
this.logger.log(
|
|
319
|
+
'error',
|
|
320
|
+
`Reconnected process did not send ready event within ${this.options.readyTimeoutMs}ms`,
|
|
321
|
+
);
|
|
322
|
+
requestConfirmedCleanup();
|
|
323
|
+
}, this.options.readyTimeoutMs);
|
|
324
|
+
this.emit('reconnected');
|
|
325
|
+
});
|
|
144
326
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
327
|
+
transportArg.on('reconnect_failed', () => {
|
|
328
|
+
if (!isCurrent()) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (reconnectReadyTimeout) {
|
|
332
|
+
clearTimeout(reconnectReadyTimeout);
|
|
333
|
+
reconnectReadyTimeout = null;
|
|
334
|
+
}
|
|
335
|
+
this.isRunning = false;
|
|
336
|
+
this.transport = null;
|
|
337
|
+
this.lifecycleState = 'idle';
|
|
338
|
+
this.rejectAllPendingRequests(`${this.options.binaryName} reconnect failed`);
|
|
339
|
+
this.emit('exit');
|
|
340
|
+
settle(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
readyTimeout = setTimeout(() => {
|
|
344
|
+
if (!isCurrent() || this.lifecycleState !== 'starting') {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.logger.log(
|
|
348
|
+
'error',
|
|
349
|
+
`Process did not send ready event within ${this.options.readyTimeoutMs}ms`,
|
|
350
|
+
);
|
|
351
|
+
requestConfirmedCleanup();
|
|
352
|
+
}, this.options.readyTimeoutMs);
|
|
353
|
+
|
|
354
|
+
let connectionPromise: Promise<void>;
|
|
355
|
+
try {
|
|
356
|
+
connectionPromise = transportArg.connect();
|
|
357
|
+
} catch (error: unknown) {
|
|
358
|
+
this.logger.log('error', `Transport connect failed: ${this.getErrorMessage(error)}`);
|
|
359
|
+
requestConfirmedCleanup();
|
|
360
|
+
return;
|
|
168
361
|
}
|
|
362
|
+
void connectionPromise.catch((error: unknown) => {
|
|
363
|
+
if (!isCurrent()) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this.logger.log('error', `Transport connect failed: ${this.getErrorMessage(error)}`);
|
|
367
|
+
requestConfirmedCleanup();
|
|
368
|
+
});
|
|
169
369
|
});
|
|
170
370
|
}
|
|
171
371
|
|
|
@@ -349,22 +549,94 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
349
549
|
}
|
|
350
550
|
|
|
351
551
|
/**
|
|
352
|
-
*
|
|
353
|
-
* For stdio
|
|
354
|
-
* For socket
|
|
552
|
+
* Terminate the active transport and wait for confirmed closure.
|
|
553
|
+
* For stdio this confirms child exit after SIGTERM/SIGKILL escalation.
|
|
554
|
+
* For socket mode this confirms local socket closure and never kills the daemon.
|
|
355
555
|
*/
|
|
356
|
-
public
|
|
357
|
-
if (
|
|
358
|
-
|
|
556
|
+
public terminate(optionsArg: IRustBridgeTerminateOptions = {}): Promise<void> {
|
|
557
|
+
if (
|
|
558
|
+
optionsArg.gracePeriodMs !== undefined
|
|
559
|
+
&& (!Number.isSafeInteger(optionsArg.gracePeriodMs) || optionsArg.gracePeriodMs < 0)
|
|
560
|
+
) {
|
|
561
|
+
throw new RangeError('RustBridge terminate gracePeriodMs must be a non-negative safe integer');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (this.terminationPromise) {
|
|
565
|
+
return this.terminationPromise;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (
|
|
569
|
+
this.lifecycleState === 'idle'
|
|
570
|
+
&& !this.transport
|
|
571
|
+
&& !this.activationPromise
|
|
572
|
+
) {
|
|
573
|
+
return Promise.resolve();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const generation = this.lifecycleGeneration;
|
|
577
|
+
const transport = this.transport;
|
|
578
|
+
const activationPromise = this.activationPromise;
|
|
579
|
+
|
|
580
|
+
this.lifecycleState = 'stopping';
|
|
581
|
+
this.isRunning = false;
|
|
582
|
+
this.rejectAllPendingRequests(`${this.options.binaryName} process killed`);
|
|
583
|
+
|
|
584
|
+
const terminationPromise = (async (): Promise<void> => {
|
|
585
|
+
if (transport) {
|
|
586
|
+
await transport.disconnectAndWait({ gracePeriodMs: optionsArg.gracePeriodMs });
|
|
587
|
+
} else if (activationPromise) {
|
|
588
|
+
// No transport exists yet (for example, binary lookup is pending). The
|
|
589
|
+
// stopping state prevents that activation from publishing a transport.
|
|
590
|
+
await activationPromise;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (this.lifecycleGeneration !== generation) {
|
|
594
|
+
throw new Error('RustBridge lifecycle generation changed during termination');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const lateTransport = this.transport;
|
|
598
|
+
if (lateTransport && lateTransport !== transport) {
|
|
599
|
+
await lateTransport.disconnectAndWait({ gracePeriodMs: optionsArg.gracePeriodMs });
|
|
600
|
+
}
|
|
601
|
+
|
|
359
602
|
this.transport = null;
|
|
360
603
|
this.isRunning = false;
|
|
604
|
+
this.lifecycleState = 'idle';
|
|
605
|
+
})();
|
|
606
|
+
|
|
607
|
+
this.terminationPromise = terminationPromise;
|
|
608
|
+
void terminationPromise.then(
|
|
609
|
+
() => {
|
|
610
|
+
if (this.terminationPromise === terminationPromise) {
|
|
611
|
+
this.terminationPromise = null;
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
() => {
|
|
615
|
+
if (this.terminationPromise === terminationPromise) {
|
|
616
|
+
// Retain transport ownership and the stopping state, but allow a
|
|
617
|
+
// later termination attempt after the caller has addressed the
|
|
618
|
+
// transient cleanup failure.
|
|
619
|
+
this.terminationPromise = null;
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
);
|
|
623
|
+
return terminationPromise;
|
|
624
|
+
}
|
|
361
625
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
626
|
+
/**
|
|
627
|
+
* Fire-and-forget compatibility wrapper around confirmed termination.
|
|
628
|
+
*/
|
|
629
|
+
public kill(): void {
|
|
630
|
+
let termination: Promise<void>;
|
|
631
|
+
try {
|
|
632
|
+
termination = this.terminate();
|
|
633
|
+
} catch (error: unknown) {
|
|
634
|
+
this.logger.log('error', `Bridge termination failed: ${this.getErrorMessage(error)}`);
|
|
635
|
+
return;
|
|
367
636
|
}
|
|
637
|
+
void termination.catch((error: unknown) => {
|
|
638
|
+
this.logger.log('error', `Bridge termination failed: ${this.getErrorMessage(error)}`);
|
|
639
|
+
});
|
|
368
640
|
}
|
|
369
641
|
|
|
370
642
|
/**
|
|
@@ -374,7 +646,24 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
374
646
|
return this.isRunning;
|
|
375
647
|
}
|
|
376
648
|
|
|
377
|
-
private
|
|
649
|
+
private isStartingGeneration(generationArg: number): boolean {
|
|
650
|
+
return (
|
|
651
|
+
this.lifecycleGeneration === generationArg
|
|
652
|
+
&& this.lifecycleState === 'starting'
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private isCurrentTransport(
|
|
657
|
+
transportArg: TConfirmableRustTransport,
|
|
658
|
+
generationArg: number,
|
|
659
|
+
): boolean {
|
|
660
|
+
return (
|
|
661
|
+
this.lifecycleGeneration === generationArg
|
|
662
|
+
&& this.transport === transportArg
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private handleLine(line: string, onReadyArg: () => void): void {
|
|
378
667
|
if (!line) return;
|
|
379
668
|
|
|
380
669
|
let parsed: any;
|
|
@@ -388,6 +677,9 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
388
677
|
// Check if it's an event (has 'event' field, no 'id')
|
|
389
678
|
if ('event' in parsed && !('id' in parsed)) {
|
|
390
679
|
const event = parsed as IManagementEvent;
|
|
680
|
+
if (event.event === this.options.readyEventName) {
|
|
681
|
+
onReadyArg();
|
|
682
|
+
}
|
|
391
683
|
this.emit(`management:${event.event}`, event.data);
|
|
392
684
|
return;
|
|
393
685
|
}
|
|
@@ -488,13 +780,6 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
|
|
|
488
780
|
}
|
|
489
781
|
}
|
|
490
782
|
|
|
491
|
-
private cleanup(message: string, cause?: unknown): void {
|
|
492
|
-
this.isRunning = false;
|
|
493
|
-
this.transport = null;
|
|
494
|
-
|
|
495
|
-
this.rejectAllPendingRequests(message, cause);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
783
|
private getErrorMessage(error: unknown): string {
|
|
499
784
|
return error instanceof Error ? error.message : String(error);
|
|
500
785
|
}
|