@push.rocks/smartrust 1.4.1 → 1.6.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.rustbridge.d.ts +19 -8
- package/dist_ts/classes.rustbridge.js +302 -78
- package/dist_ts/classes.sockettransport.d.ts +16 -2
- package/dist_ts/classes.sockettransport.js +234 -61
- package/dist_ts/classes.stdiotransport.d.ts +15 -3
- package/dist_ts/classes.stdiotransport.js +253 -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 +30 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.rustbridge.ts +363 -83
- package/ts/classes.sockettransport.ts +304 -60
- package/ts/classes.stdiotransport.ts +345 -58
- package/ts/interfaces/config.ts +9 -0
- package/ts/interfaces/transport.ts +12 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { LineScanner } from './classes.linescanner.js';
|
|
3
3
|
import type { IRustBridgeLogger } from './interfaces/index.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
IRustTransport,
|
|
6
|
+
IRustTransportDisconnectOptions,
|
|
7
|
+
} from './interfaces/transport.js';
|
|
5
8
|
|
|
6
9
|
export interface ISocketTransportOptions {
|
|
7
10
|
/** Path to Unix socket (Linux/macOS) or named pipe path (Windows) */
|
|
@@ -30,6 +33,18 @@ interface IResolvedSocketTransportOptions {
|
|
|
30
33
|
maxReconnectAttempts: number;
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
interface ISocketTransportGeneration {
|
|
37
|
+
socket: plugins.net.Socket;
|
|
38
|
+
lifecycleToken: number;
|
|
39
|
+
reconnectAttempt: boolean;
|
|
40
|
+
everConnected: boolean;
|
|
41
|
+
connectSettled: boolean;
|
|
42
|
+
closed: boolean;
|
|
43
|
+
closePromise: Promise<void>;
|
|
44
|
+
resolveClose: () => void;
|
|
45
|
+
disconnectPromise: Promise<void> | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
/**
|
|
34
49
|
* Transport that connects to an already-running process via Unix socket or Windows named pipe.
|
|
35
50
|
* The JSON-over-newline protocol is identical to stdio; only the transport changes.
|
|
@@ -37,11 +52,15 @@ interface IResolvedSocketTransportOptions {
|
|
|
37
52
|
export class SocketTransport extends plugins.events.EventEmitter implements IRustTransport {
|
|
38
53
|
private options: IResolvedSocketTransportOptions;
|
|
39
54
|
private socket: plugins.net.Socket | null = null;
|
|
55
|
+
private generation: ISocketTransportGeneration | null = null;
|
|
40
56
|
private lineScanner: LineScanner;
|
|
41
57
|
private _connected: boolean = false;
|
|
42
58
|
private intentionalDisconnect: boolean = false;
|
|
43
59
|
private reconnectAttempts: number = 0;
|
|
44
60
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
61
|
+
private lifecycleToken: number = 0;
|
|
62
|
+
private _reconnecting: boolean = false;
|
|
63
|
+
private reconnectFailed: boolean = false;
|
|
45
64
|
|
|
46
65
|
constructor(options: ISocketTransportOptions) {
|
|
47
66
|
super();
|
|
@@ -59,105 +78,274 @@ export class SocketTransport extends plugins.events.EventEmitter implements IRus
|
|
|
59
78
|
return this._connected;
|
|
60
79
|
}
|
|
61
80
|
|
|
81
|
+
public get reconnecting(): boolean {
|
|
82
|
+
return !this.intentionalDisconnect && this._reconnecting;
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
/**
|
|
63
86
|
* Connect to the socket. Resolves when the TCP/Unix connection is established.
|
|
64
87
|
*/
|
|
65
|
-
public
|
|
88
|
+
public connect(): Promise<void> {
|
|
89
|
+
if (this.generation && !this.generation.closed) {
|
|
90
|
+
return Promise.reject(
|
|
91
|
+
new Error('Socket transport already has an active connection attempt or connection'),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.cancelReconnectTimer();
|
|
96
|
+
const lifecycleToken = ++this.lifecycleToken;
|
|
66
97
|
this.intentionalDisconnect = false;
|
|
98
|
+
this._reconnecting = false;
|
|
99
|
+
this.reconnectFailed = false;
|
|
67
100
|
this.reconnectAttempts = 0;
|
|
68
|
-
return this.doConnect();
|
|
101
|
+
return this.doConnect(lifecycleToken, false);
|
|
69
102
|
}
|
|
70
103
|
|
|
71
|
-
private doConnect(
|
|
72
|
-
|
|
73
|
-
|
|
104
|
+
private doConnect(
|
|
105
|
+
lifecycleToken: number,
|
|
106
|
+
reconnectAttempt: boolean,
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
if (
|
|
109
|
+
lifecycleToken !== this.lifecycleToken
|
|
110
|
+
|| this.intentionalDisconnect
|
|
111
|
+
) {
|
|
112
|
+
return Promise.reject(new Error('Socket connection attempt was cancelled'));
|
|
113
|
+
}
|
|
114
|
+
if (this.generation && !this.generation.closed) {
|
|
115
|
+
return Promise.reject(
|
|
116
|
+
new Error('Socket transport already has an active connection attempt or connection'),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
74
119
|
|
|
75
|
-
|
|
120
|
+
const socket = new plugins.net.Socket();
|
|
121
|
+
let resolveConnect!: () => void;
|
|
122
|
+
let rejectConnect!: (error: Error) => void;
|
|
123
|
+
const connectPromise = new Promise<void>((resolve, reject) => {
|
|
124
|
+
resolveConnect = resolve;
|
|
125
|
+
rejectConnect = reject;
|
|
126
|
+
});
|
|
127
|
+
let resolveClose!: () => void;
|
|
128
|
+
const closePromise = new Promise<void>((resolve) => {
|
|
129
|
+
resolveClose = resolve;
|
|
130
|
+
});
|
|
131
|
+
const generation: ISocketTransportGeneration = {
|
|
132
|
+
socket,
|
|
133
|
+
lifecycleToken,
|
|
134
|
+
reconnectAttempt,
|
|
135
|
+
everConnected: false,
|
|
136
|
+
connectSettled: false,
|
|
137
|
+
closed: false,
|
|
138
|
+
closePromise,
|
|
139
|
+
resolveClose,
|
|
140
|
+
disconnectPromise: null,
|
|
141
|
+
};
|
|
76
142
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
settled = true;
|
|
80
|
-
this._connected = true;
|
|
81
|
-
this.reconnectAttempts = 0;
|
|
82
|
-
resolve();
|
|
83
|
-
}
|
|
84
|
-
});
|
|
143
|
+
this.generation = generation;
|
|
144
|
+
this.socket = socket;
|
|
85
145
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
});
|
|
146
|
+
socket.on('connect', () => {
|
|
147
|
+
if (this.generation !== generation || generation.closed) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
91
150
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
this.
|
|
151
|
+
generation.everConnected = true;
|
|
152
|
+
this._connected = true;
|
|
153
|
+
if (generation.reconnectAttempt) {
|
|
154
|
+
this._reconnecting = false;
|
|
155
|
+
}
|
|
156
|
+
this.reconnectAttempts = 0;
|
|
157
|
+
if (!generation.connectSettled) {
|
|
158
|
+
generation.connectSettled = true;
|
|
159
|
+
resolveConnect();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
96
162
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
163
|
+
socket.on('data', (chunk: Buffer) => {
|
|
164
|
+
if (this.generation !== generation || generation.closed) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
102
167
|
|
|
103
|
-
this.
|
|
104
|
-
this.
|
|
105
|
-
|
|
106
|
-
settled = true;
|
|
107
|
-
reject(err);
|
|
108
|
-
} else if (!this.intentionalDisconnect) {
|
|
109
|
-
this.emit('error', err);
|
|
168
|
+
this.lineScanner.push(chunk, (line) => {
|
|
169
|
+
if (this.generation === generation && !generation.closed) {
|
|
170
|
+
this.emit('line', line);
|
|
110
171
|
}
|
|
111
172
|
});
|
|
112
173
|
});
|
|
174
|
+
|
|
175
|
+
socket.on('close', () => {
|
|
176
|
+
if (generation.closed) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
generation.closed = true;
|
|
181
|
+
if (!generation.connectSettled) {
|
|
182
|
+
generation.connectSettled = true;
|
|
183
|
+
rejectConnect(new Error('Socket closed before connection was established'));
|
|
184
|
+
}
|
|
185
|
+
generation.resolveClose();
|
|
186
|
+
|
|
187
|
+
if (this.generation !== generation) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this._connected = false;
|
|
192
|
+
this.socket = null;
|
|
193
|
+
this.lineScanner.clear();
|
|
194
|
+
|
|
195
|
+
const shouldReconnect = (
|
|
196
|
+
!this.intentionalDisconnect
|
|
197
|
+
&& generation.lifecycleToken === this.lifecycleToken
|
|
198
|
+
&& this.options.autoReconnect
|
|
199
|
+
&& (generation.everConnected || generation.reconnectAttempt)
|
|
200
|
+
);
|
|
201
|
+
if (shouldReconnect) {
|
|
202
|
+
this._reconnecting = true;
|
|
203
|
+
}
|
|
204
|
+
this.emit('close');
|
|
205
|
+
|
|
206
|
+
// A close listener may intentionally disconnect or start a new generation.
|
|
207
|
+
// Only the still-current closed generation may schedule the retry.
|
|
208
|
+
if (
|
|
209
|
+
shouldReconnect
|
|
210
|
+
&& this.generation === generation
|
|
211
|
+
&& !this.intentionalDisconnect
|
|
212
|
+
&& generation.lifecycleToken === this.lifecycleToken
|
|
213
|
+
) {
|
|
214
|
+
this.attemptReconnect(generation.lifecycleToken);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
socket.on('error', (error: Error) => {
|
|
219
|
+
if (this.generation !== generation || generation.closed) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this._connected = false;
|
|
224
|
+
if (!generation.connectSettled) {
|
|
225
|
+
generation.connectSettled = true;
|
|
226
|
+
rejectConnect(error);
|
|
227
|
+
} else if (!this.intentionalDisconnect) {
|
|
228
|
+
this.emit('error', error);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
socket.connect({ path: this.options.socketPath });
|
|
234
|
+
} catch (error: unknown) {
|
|
235
|
+
const connectError = this.toError(error, 'Failed to initiate socket connection');
|
|
236
|
+
generation.closed = true;
|
|
237
|
+
if (!generation.connectSettled) {
|
|
238
|
+
generation.connectSettled = true;
|
|
239
|
+
rejectConnect(connectError);
|
|
240
|
+
}
|
|
241
|
+
generation.resolveClose();
|
|
242
|
+
if (this.generation === generation) {
|
|
243
|
+
this._connected = false;
|
|
244
|
+
this.socket = null;
|
|
245
|
+
this.lineScanner.clear();
|
|
246
|
+
}
|
|
247
|
+
if (
|
|
248
|
+
reconnectAttempt
|
|
249
|
+
&& !this.intentionalDisconnect
|
|
250
|
+
&& lifecycleToken === this.lifecycleToken
|
|
251
|
+
) {
|
|
252
|
+
this.attemptReconnect(lifecycleToken);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return connectPromise;
|
|
113
257
|
}
|
|
114
258
|
|
|
115
|
-
private attemptReconnect(): void {
|
|
259
|
+
private attemptReconnect(lifecycleToken: number): void {
|
|
260
|
+
if (
|
|
261
|
+
lifecycleToken !== this.lifecycleToken
|
|
262
|
+
|| this.intentionalDisconnect
|
|
263
|
+
|| this.reconnectFailed
|
|
264
|
+
|| this.reconnectTimer
|
|
265
|
+
|| (this.generation && !this.generation.closed)
|
|
266
|
+
) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
116
270
|
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
|
|
117
|
-
this.
|
|
271
|
+
this._reconnecting = false;
|
|
272
|
+
this.reconnectFailed = true;
|
|
273
|
+
++this.lifecycleToken;
|
|
274
|
+
this.options.logger.log(
|
|
275
|
+
'error',
|
|
276
|
+
'Max reconnect attempts (' + this.options.maxReconnectAttempts + ') reached',
|
|
277
|
+
);
|
|
118
278
|
this.emit('reconnect_failed');
|
|
119
279
|
return;
|
|
120
280
|
}
|
|
121
281
|
|
|
282
|
+
this._reconnecting = true;
|
|
283
|
+
|
|
122
284
|
const delay = Math.min(
|
|
123
285
|
this.options.reconnectBaseDelayMs * Math.pow(2, this.reconnectAttempts),
|
|
124
286
|
this.options.reconnectMaxDelayMs,
|
|
125
287
|
);
|
|
126
288
|
this.reconnectAttempts++;
|
|
127
289
|
|
|
128
|
-
this.options.logger.log(
|
|
290
|
+
this.options.logger.log(
|
|
291
|
+
'info',
|
|
292
|
+
'Reconnecting in '
|
|
293
|
+
+ delay
|
|
294
|
+
+ 'ms (attempt '
|
|
295
|
+
+ this.reconnectAttempts
|
|
296
|
+
+ '/'
|
|
297
|
+
+ this.options.maxReconnectAttempts
|
|
298
|
+
+ ')',
|
|
299
|
+
);
|
|
129
300
|
|
|
130
|
-
this.reconnectTimer = setTimeout(
|
|
301
|
+
this.reconnectTimer = setTimeout(() => {
|
|
131
302
|
this.reconnectTimer = null;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
this.
|
|
135
|
-
|
|
136
|
-
|
|
303
|
+
if (
|
|
304
|
+
lifecycleToken !== this.lifecycleToken
|
|
305
|
+
|| this.intentionalDisconnect
|
|
306
|
+
) {
|
|
307
|
+
return;
|
|
137
308
|
}
|
|
309
|
+
|
|
310
|
+
void this.doConnect(lifecycleToken, true).then(() => {
|
|
311
|
+
if (
|
|
312
|
+
lifecycleToken === this.lifecycleToken
|
|
313
|
+
&& !this.intentionalDisconnect
|
|
314
|
+
) {
|
|
315
|
+
this.emit('reconnected');
|
|
316
|
+
}
|
|
317
|
+
}).catch(() => {
|
|
318
|
+
// The generation's close handler schedules the next reconnect attempt.
|
|
319
|
+
});
|
|
138
320
|
}, delay);
|
|
139
321
|
}
|
|
140
322
|
|
|
141
323
|
/**
|
|
142
324
|
* Write data to the socket with backpressure support.
|
|
143
325
|
*/
|
|
144
|
-
public
|
|
326
|
+
public write(data: string): Promise<void> {
|
|
145
327
|
return new Promise<void>((resolve, reject) => {
|
|
146
|
-
|
|
328
|
+
const generation = this.generation;
|
|
329
|
+
if (
|
|
330
|
+
!generation
|
|
331
|
+
|| generation.closed
|
|
332
|
+
|| generation.socket !== this.socket
|
|
333
|
+
|| !this._connected
|
|
334
|
+
) {
|
|
147
335
|
reject(new Error('Socket not connected'));
|
|
148
336
|
return;
|
|
149
337
|
}
|
|
150
338
|
|
|
151
|
-
const canContinue =
|
|
152
|
-
if (
|
|
153
|
-
reject(
|
|
339
|
+
const canContinue = generation.socket.write(data, 'utf8', (error) => {
|
|
340
|
+
if (error) {
|
|
341
|
+
reject(error);
|
|
154
342
|
}
|
|
155
343
|
});
|
|
156
344
|
|
|
157
345
|
if (canContinue) {
|
|
158
346
|
resolve();
|
|
159
347
|
} else {
|
|
160
|
-
|
|
348
|
+
generation.socket.once('drain', () => {
|
|
161
349
|
resolve();
|
|
162
350
|
});
|
|
163
351
|
}
|
|
@@ -165,23 +353,79 @@ export class SocketTransport extends plugins.events.EventEmitter implements IRus
|
|
|
165
353
|
}
|
|
166
354
|
|
|
167
355
|
/**
|
|
168
|
-
* Close the socket
|
|
356
|
+
* Close the local socket without waiting for confirmation.
|
|
357
|
+
* This does not kill the remote daemon.
|
|
169
358
|
*/
|
|
170
359
|
public disconnect(): void {
|
|
360
|
+
try {
|
|
361
|
+
void this.disconnectAndWait().catch((error: unknown) => {
|
|
362
|
+
this.options.logger.log(
|
|
363
|
+
'error',
|
|
364
|
+
'Failed to disconnect socket transport: ' + this.getErrorMessage(error),
|
|
365
|
+
);
|
|
366
|
+
});
|
|
367
|
+
} catch (error: unknown) {
|
|
368
|
+
this.options.logger.log(
|
|
369
|
+
'error',
|
|
370
|
+
'Failed to disconnect socket transport: ' + this.getErrorMessage(error),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Destroy the local socket and wait until its 'close' event confirms closure.
|
|
377
|
+
* This does not signal or otherwise terminate the remote daemon.
|
|
378
|
+
*/
|
|
379
|
+
public disconnectAndWait(
|
|
380
|
+
_options: IRustTransportDisconnectOptions = {},
|
|
381
|
+
): Promise<void> {
|
|
171
382
|
this.intentionalDisconnect = true;
|
|
383
|
+
this._reconnecting = false;
|
|
384
|
+
++this.lifecycleToken;
|
|
385
|
+
this.cancelReconnectTimer();
|
|
386
|
+
|
|
387
|
+
const generation = this.generation;
|
|
388
|
+
if (!generation) {
|
|
389
|
+
return Promise.resolve();
|
|
390
|
+
}
|
|
391
|
+
if (generation.disconnectPromise) {
|
|
392
|
+
return generation.disconnectPromise;
|
|
393
|
+
}
|
|
394
|
+
if (generation.closed) {
|
|
395
|
+
generation.disconnectPromise = generation.closePromise;
|
|
396
|
+
return generation.disconnectPromise;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
this._connected = false;
|
|
400
|
+
let rejectDisconnect!: (error: Error) => void;
|
|
401
|
+
const disconnectPromise = new Promise<void>((resolve, reject) => {
|
|
402
|
+
rejectDisconnect = reject;
|
|
403
|
+
void generation.closePromise.then(resolve);
|
|
404
|
+
});
|
|
405
|
+
generation.disconnectPromise = disconnectPromise;
|
|
172
406
|
|
|
407
|
+
// The close waiter above must exist before destroy() can cause 'close'.
|
|
408
|
+
try {
|
|
409
|
+
generation.socket.destroy();
|
|
410
|
+
} catch (error: unknown) {
|
|
411
|
+
rejectDisconnect(this.toError(error, 'Failed to destroy local socket'));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return disconnectPromise;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private cancelReconnectTimer(): void {
|
|
173
418
|
if (this.reconnectTimer) {
|
|
174
419
|
clearTimeout(this.reconnectTimer);
|
|
175
420
|
this.reconnectTimer = null;
|
|
176
421
|
}
|
|
422
|
+
}
|
|
177
423
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
sock.destroy();
|
|
185
|
-
}
|
|
424
|
+
private toError(error: unknown, prefix: string): Error {
|
|
425
|
+
return new Error(prefix + ': ' + this.getErrorMessage(error));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private getErrorMessage(error: unknown): string {
|
|
429
|
+
return error instanceof Error ? error.message : String(error);
|
|
186
430
|
}
|
|
187
431
|
}
|