@push.rocks/smartrust 1.2.0 → 1.3.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 +18 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.linescanner.d.ts +18 -0
- package/dist_ts/classes.linescanner.js +43 -0
- package/dist_ts/classes.rustbinarylocator.js +12 -2
- package/dist_ts/classes.rustbridge.d.ts +19 -16
- package/dist_ts/classes.rustbridge.js +86 -152
- package/dist_ts/classes.sockettransport.d.ts +48 -0
- package/dist_ts/classes.sockettransport.js +138 -0
- package/dist_ts/classes.stdiotransport.d.ts +35 -0
- package/dist_ts/classes.stdiotransport.js +136 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -1
- package/dist_ts/interfaces/config.d.ts +13 -0
- package/dist_ts/interfaces/index.d.ts +1 -0
- package/dist_ts/interfaces/index.js +2 -1
- package/dist_ts/interfaces/transport.d.ts +22 -0
- package/dist_ts/interfaces/transport.js +2 -0
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/package.json +1 -1
- package/test/helpers/mock-socket-server.mjs +124 -0
- package/test/test.linescanner.node.ts +89 -0
- package/test/test.sockettransport.node.ts +312 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.linescanner.ts +51 -0
- package/ts/classes.rustbinarylocator.ts +10 -1
- package/ts/classes.rustbridge.ts +93 -147
- package/ts/classes.sockettransport.ts +187 -0
- package/ts/classes.stdiotransport.ts +149 -0
- package/ts/index.ts +3 -0
- package/ts/interfaces/config.ts +14 -0
- package/ts/interfaces/index.ts +1 -0
- package/ts/interfaces/transport.ts +26 -0
- package/ts/plugins.ts +2 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
2
|
+
import { LineScanner } from '../ts/classes.linescanner.js';
|
|
3
|
+
|
|
4
|
+
const noopLogger = { log() {} };
|
|
5
|
+
|
|
6
|
+
tap.test('should parse a single complete line', async () => {
|
|
7
|
+
const scanner = new LineScanner(1024 * 1024, noopLogger);
|
|
8
|
+
const lines: string[] = [];
|
|
9
|
+
scanner.push(Buffer.from('{"hello":"world"}\n'), (line) => lines.push(line));
|
|
10
|
+
expect(lines.length).toEqual(1);
|
|
11
|
+
expect(lines[0]).toEqual('{"hello":"world"}');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
tap.test('should parse multiple lines in one chunk', async () => {
|
|
15
|
+
const scanner = new LineScanner(1024 * 1024, noopLogger);
|
|
16
|
+
const lines: string[] = [];
|
|
17
|
+
scanner.push(Buffer.from('{"a":1}\n{"b":2}\n{"c":3}\n'), (line) => lines.push(line));
|
|
18
|
+
expect(lines.length).toEqual(3);
|
|
19
|
+
expect(lines[0]).toEqual('{"a":1}');
|
|
20
|
+
expect(lines[1]).toEqual('{"b":2}');
|
|
21
|
+
expect(lines[2]).toEqual('{"c":3}');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
tap.test('should handle a line split across chunks', async () => {
|
|
25
|
+
const scanner = new LineScanner(1024 * 1024, noopLogger);
|
|
26
|
+
const lines: string[] = [];
|
|
27
|
+
scanner.push(Buffer.from('{"hel'), (line) => lines.push(line));
|
|
28
|
+
expect(lines.length).toEqual(0);
|
|
29
|
+
scanner.push(Buffer.from('lo":"world"}\n'), (line) => lines.push(line));
|
|
30
|
+
expect(lines.length).toEqual(1);
|
|
31
|
+
expect(lines[0]).toEqual('{"hello":"world"}');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
tap.test('should drop oversized lines', async () => {
|
|
35
|
+
const scanner = new LineScanner(100, noopLogger);
|
|
36
|
+
const lines: string[] = [];
|
|
37
|
+
// Line is 200 chars + newline, exceeds maxPayloadSize of 100
|
|
38
|
+
const oversized = 'x'.repeat(200) + '\n';
|
|
39
|
+
scanner.push(Buffer.from(oversized), (line) => lines.push(line));
|
|
40
|
+
expect(lines.length).toEqual(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
tap.test('should clear buffer on OOM (no newline, exceeds max)', async () => {
|
|
44
|
+
const scanner = new LineScanner(100, noopLogger);
|
|
45
|
+
const lines: string[] = [];
|
|
46
|
+
// Push 200 bytes without any newline — exceeds maxPayloadSize
|
|
47
|
+
scanner.push(Buffer.from('x'.repeat(200)), (line) => lines.push(line));
|
|
48
|
+
expect(lines.length).toEqual(0);
|
|
49
|
+
// After clearing, should work normally again
|
|
50
|
+
scanner.push(Buffer.from('{"ok":true}\n'), (line) => lines.push(line));
|
|
51
|
+
expect(lines.length).toEqual(1);
|
|
52
|
+
expect(lines[0]).toEqual('{"ok":true}');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
tap.test('should skip empty lines', async () => {
|
|
56
|
+
const scanner = new LineScanner(1024 * 1024, noopLogger);
|
|
57
|
+
const lines: string[] = [];
|
|
58
|
+
scanner.push(Buffer.from('\n\n{"a":1}\n\n'), (line) => lines.push(line));
|
|
59
|
+
expect(lines.length).toEqual(1);
|
|
60
|
+
expect(lines[0]).toEqual('{"a":1}');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
tap.test('should handle mixed complete and partial lines', async () => {
|
|
64
|
+
const scanner = new LineScanner(1024 * 1024, noopLogger);
|
|
65
|
+
const lines: string[] = [];
|
|
66
|
+
// First chunk: one complete line + start of second line
|
|
67
|
+
scanner.push(Buffer.from('{"first":1}\n{"sec'), (line) => lines.push(line));
|
|
68
|
+
expect(lines.length).toEqual(1);
|
|
69
|
+
// Second chunk: end of second line + complete third line
|
|
70
|
+
scanner.push(Buffer.from('ond":2}\n{"third":3}\n'), (line) => lines.push(line));
|
|
71
|
+
expect(lines.length).toEqual(3);
|
|
72
|
+
expect(lines[1]).toEqual('{"second":2}');
|
|
73
|
+
expect(lines[2]).toEqual('{"third":3}');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
tap.test('clear should reset the buffer', async () => {
|
|
77
|
+
const scanner = new LineScanner(1024 * 1024, noopLogger);
|
|
78
|
+
const lines: string[] = [];
|
|
79
|
+
// Push partial data
|
|
80
|
+
scanner.push(Buffer.from('{"partial":'), (line) => lines.push(line));
|
|
81
|
+
// Clear
|
|
82
|
+
scanner.clear();
|
|
83
|
+
// Now push a complete new line — old partial should not affect it
|
|
84
|
+
scanner.push(Buffer.from('{"fresh":true}\n'), (line) => lines.push(line));
|
|
85
|
+
expect(lines.length).toEqual(1);
|
|
86
|
+
expect(lines[0]).toEqual('{"fresh":true}');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export default tap.start();
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as childProcess from 'child_process';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { RustBridge } from '../ts/classes.rustbridge.js';
|
|
7
|
+
import type { ICommandDefinition } from '../ts/interfaces/index.js';
|
|
8
|
+
|
|
9
|
+
const testDir = path.resolve(path.dirname(new URL(import.meta.url).pathname));
|
|
10
|
+
const mockServerPath = path.join(testDir, 'helpers/mock-socket-server.mjs');
|
|
11
|
+
|
|
12
|
+
// Define the command types for our mock binary
|
|
13
|
+
type TMockCommands = {
|
|
14
|
+
echo: { params: Record<string, any>; result: Record<string, any> };
|
|
15
|
+
largeEcho: { params: Record<string, any>; result: Record<string, any> };
|
|
16
|
+
error: { params: {}; result: never };
|
|
17
|
+
emitEvent: { params: { eventName: string; eventData: any }; result: null };
|
|
18
|
+
slow: { params: {}; result: { delayed: boolean } };
|
|
19
|
+
exit: { params: {}; result: null };
|
|
20
|
+
streamEcho: { params: { count: number }; chunk: { index: number; value: string }; result: { totalChunks: number } };
|
|
21
|
+
streamError: { params: {}; chunk: { index: number; value: string }; result: never };
|
|
22
|
+
streamEmpty: { params: {}; chunk: never; result: { totalChunks: number } };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Start the mock socket server and return the socket path.
|
|
27
|
+
* Returns { proc, socketPath }.
|
|
28
|
+
*/
|
|
29
|
+
async function startMockServer(testName: string): Promise<{ proc: childProcess.ChildProcess; socketPath: string }> {
|
|
30
|
+
const socketPath = path.join(os.tmpdir(), `smartrust-test-${Date.now()}-${testName}.sock`);
|
|
31
|
+
|
|
32
|
+
const proc = childProcess.spawn('node', [mockServerPath, socketPath], {
|
|
33
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Wait for the server to signal readiness via stdout
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let stdoutData = '';
|
|
39
|
+
const timeout = setTimeout(() => {
|
|
40
|
+
proc.kill();
|
|
41
|
+
reject(new Error('Mock server did not start within 5s'));
|
|
42
|
+
}, 5000);
|
|
43
|
+
|
|
44
|
+
proc.stdout!.on('data', (data: Buffer) => {
|
|
45
|
+
stdoutData += data.toString();
|
|
46
|
+
const lines = stdoutData.split('\n');
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
if (line.trim()) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(line.trim());
|
|
51
|
+
if (parsed.ready) {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
resolve({ proc, socketPath: parsed.socketPath });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
} catch { /* not JSON yet */ }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
proc.on('error', (err) => {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
reject(err);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
proc.on('exit', (code) => {
|
|
67
|
+
clearTimeout(timeout);
|
|
68
|
+
reject(new Error(`Mock server exited with code ${code}`));
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stopMockServer(proc: childProcess.ChildProcess, socketPath: string) {
|
|
74
|
+
try { proc.kill('SIGTERM'); } catch { /* ignore */ }
|
|
75
|
+
try { fs.unlinkSync(socketPath); } catch { /* ignore */ }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// === Socket Transport Tests via RustBridge.connect() ===
|
|
79
|
+
|
|
80
|
+
tap.test('socket: should connect and receive ready event', async () => {
|
|
81
|
+
const { proc, socketPath } = await startMockServer('connect-ready');
|
|
82
|
+
try {
|
|
83
|
+
const bridge = new RustBridge<TMockCommands>({
|
|
84
|
+
binaryName: 'mock-daemon',
|
|
85
|
+
readyTimeoutMs: 5000,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const result = await bridge.connect(socketPath);
|
|
89
|
+
expect(result).toBeTrue();
|
|
90
|
+
expect(bridge.running).toBeTrue();
|
|
91
|
+
|
|
92
|
+
bridge.kill();
|
|
93
|
+
expect(bridge.running).toBeFalse();
|
|
94
|
+
} finally {
|
|
95
|
+
stopMockServer(proc, socketPath);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
tap.test('socket: should send command and receive response', async () => {
|
|
100
|
+
const { proc, socketPath } = await startMockServer('send-command');
|
|
101
|
+
try {
|
|
102
|
+
const bridge = new RustBridge<TMockCommands>({
|
|
103
|
+
binaryName: 'mock-daemon',
|
|
104
|
+
readyTimeoutMs: 5000,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await bridge.connect(socketPath);
|
|
108
|
+
|
|
109
|
+
const result = await bridge.sendCommand('echo', { hello: 'world', num: 42 });
|
|
110
|
+
expect(result).toEqual({ hello: 'world', num: 42 });
|
|
111
|
+
|
|
112
|
+
bridge.kill();
|
|
113
|
+
} finally {
|
|
114
|
+
stopMockServer(proc, socketPath);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
tap.test('socket: should handle error responses', async () => {
|
|
119
|
+
const { proc, socketPath } = await startMockServer('error-response');
|
|
120
|
+
try {
|
|
121
|
+
const bridge = new RustBridge<TMockCommands>({
|
|
122
|
+
binaryName: 'mock-daemon',
|
|
123
|
+
readyTimeoutMs: 5000,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await bridge.connect(socketPath);
|
|
127
|
+
|
|
128
|
+
let threw = false;
|
|
129
|
+
try {
|
|
130
|
+
await bridge.sendCommand('error', {});
|
|
131
|
+
} catch (err: any) {
|
|
132
|
+
threw = true;
|
|
133
|
+
expect(err.message).toInclude('Test error message');
|
|
134
|
+
}
|
|
135
|
+
expect(threw).toBeTrue();
|
|
136
|
+
|
|
137
|
+
bridge.kill();
|
|
138
|
+
} finally {
|
|
139
|
+
stopMockServer(proc, socketPath);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
tap.test('socket: should receive custom events', async () => {
|
|
144
|
+
const { proc, socketPath } = await startMockServer('custom-events');
|
|
145
|
+
try {
|
|
146
|
+
const bridge = new RustBridge<TMockCommands>({
|
|
147
|
+
binaryName: 'mock-daemon',
|
|
148
|
+
readyTimeoutMs: 5000,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await bridge.connect(socketPath);
|
|
152
|
+
|
|
153
|
+
const eventPromise = new Promise<any>((resolve) => {
|
|
154
|
+
bridge.once('management:testEvent', (data) => resolve(data));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await bridge.sendCommand('emitEvent', {
|
|
158
|
+
eventName: 'testEvent',
|
|
159
|
+
eventData: { key: 'value' },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const eventData = await eventPromise;
|
|
163
|
+
expect(eventData).toEqual({ key: 'value' });
|
|
164
|
+
|
|
165
|
+
bridge.kill();
|
|
166
|
+
} finally {
|
|
167
|
+
stopMockServer(proc, socketPath);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
tap.test('socket: should handle multiple concurrent commands', async () => {
|
|
172
|
+
const { proc, socketPath } = await startMockServer('concurrent');
|
|
173
|
+
try {
|
|
174
|
+
const bridge = new RustBridge<TMockCommands>({
|
|
175
|
+
binaryName: 'mock-daemon',
|
|
176
|
+
readyTimeoutMs: 5000,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await bridge.connect(socketPath);
|
|
180
|
+
|
|
181
|
+
const results = await Promise.all([
|
|
182
|
+
bridge.sendCommand('echo', { id: 1 }),
|
|
183
|
+
bridge.sendCommand('echo', { id: 2 }),
|
|
184
|
+
bridge.sendCommand('echo', { id: 3 }),
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
expect(results[0]).toEqual({ id: 1 });
|
|
188
|
+
expect(results[1]).toEqual({ id: 2 });
|
|
189
|
+
expect(results[2]).toEqual({ id: 3 });
|
|
190
|
+
|
|
191
|
+
bridge.kill();
|
|
192
|
+
} finally {
|
|
193
|
+
stopMockServer(proc, socketPath);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
tap.test('socket: should handle 1MB payload round-trip', async () => {
|
|
198
|
+
const { proc, socketPath } = await startMockServer('large-payload');
|
|
199
|
+
try {
|
|
200
|
+
const bridge = new RustBridge<TMockCommands>({
|
|
201
|
+
binaryName: 'mock-daemon',
|
|
202
|
+
readyTimeoutMs: 5000,
|
|
203
|
+
requestTimeoutMs: 30000,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await bridge.connect(socketPath);
|
|
207
|
+
|
|
208
|
+
const largeString = 'x'.repeat(1024 * 1024);
|
|
209
|
+
const result = await bridge.sendCommand('largeEcho', { data: largeString });
|
|
210
|
+
expect(result.data).toEqual(largeString);
|
|
211
|
+
expect(result.data.length).toEqual(1024 * 1024);
|
|
212
|
+
|
|
213
|
+
bridge.kill();
|
|
214
|
+
} finally {
|
|
215
|
+
stopMockServer(proc, socketPath);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
tap.test('socket: should disconnect without killing the daemon', async () => {
|
|
220
|
+
const { proc, socketPath } = await startMockServer('no-kill-daemon');
|
|
221
|
+
try {
|
|
222
|
+
// First connection
|
|
223
|
+
const bridge1 = new RustBridge<TMockCommands>({
|
|
224
|
+
binaryName: 'mock-daemon',
|
|
225
|
+
readyTimeoutMs: 5000,
|
|
226
|
+
});
|
|
227
|
+
await bridge1.connect(socketPath);
|
|
228
|
+
expect(bridge1.running).toBeTrue();
|
|
229
|
+
|
|
230
|
+
// Disconnect
|
|
231
|
+
bridge1.kill();
|
|
232
|
+
expect(bridge1.running).toBeFalse();
|
|
233
|
+
|
|
234
|
+
// Second connection — daemon should still be alive
|
|
235
|
+
const bridge2 = new RustBridge<TMockCommands>({
|
|
236
|
+
binaryName: 'mock-daemon',
|
|
237
|
+
readyTimeoutMs: 5000,
|
|
238
|
+
});
|
|
239
|
+
const result = await bridge2.connect(socketPath);
|
|
240
|
+
expect(result).toBeTrue();
|
|
241
|
+
expect(bridge2.running).toBeTrue();
|
|
242
|
+
|
|
243
|
+
// Verify the daemon is functional
|
|
244
|
+
const echoResult = await bridge2.sendCommand('echo', { reconnected: true });
|
|
245
|
+
expect(echoResult).toEqual({ reconnected: true });
|
|
246
|
+
|
|
247
|
+
bridge2.kill();
|
|
248
|
+
} finally {
|
|
249
|
+
stopMockServer(proc, socketPath);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
tap.test('socket: should stream responses via socket', async () => {
|
|
254
|
+
const { proc, socketPath } = await startMockServer('streaming');
|
|
255
|
+
try {
|
|
256
|
+
const bridge = new RustBridge<TMockCommands>({
|
|
257
|
+
binaryName: 'mock-daemon',
|
|
258
|
+
readyTimeoutMs: 5000,
|
|
259
|
+
requestTimeoutMs: 10000,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await bridge.connect(socketPath);
|
|
263
|
+
|
|
264
|
+
const stream = bridge.sendCommandStreaming('streamEcho', { count: 5 });
|
|
265
|
+
const chunks: Array<{ index: number; value: string }> = [];
|
|
266
|
+
|
|
267
|
+
for await (const chunk of stream) {
|
|
268
|
+
chunks.push(chunk);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
expect(chunks.length).toEqual(5);
|
|
272
|
+
for (let i = 0; i < 5; i++) {
|
|
273
|
+
expect(chunks[i].index).toEqual(i);
|
|
274
|
+
expect(chunks[i].value).toEqual(`chunk_${i}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const result = await stream.result;
|
|
278
|
+
expect(result.totalChunks).toEqual(5);
|
|
279
|
+
|
|
280
|
+
bridge.kill();
|
|
281
|
+
} finally {
|
|
282
|
+
stopMockServer(proc, socketPath);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
tap.test('socket: should return false when socket path does not exist', async () => {
|
|
287
|
+
const bridge = new RustBridge<TMockCommands>({
|
|
288
|
+
binaryName: 'mock-daemon',
|
|
289
|
+
readyTimeoutMs: 3000,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const result = await bridge.connect('/tmp/nonexistent-smartrust-test.sock');
|
|
293
|
+
expect(result).toBeFalse();
|
|
294
|
+
expect(bridge.running).toBeFalse();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
tap.test('socket: should throw when sending command while not connected', async () => {
|
|
298
|
+
const bridge = new RustBridge<TMockCommands>({
|
|
299
|
+
binaryName: 'mock-daemon',
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
let threw = false;
|
|
303
|
+
try {
|
|
304
|
+
await bridge.sendCommand('echo', {});
|
|
305
|
+
} catch (err: any) {
|
|
306
|
+
threw = true;
|
|
307
|
+
expect(err.message).toInclude('not running');
|
|
308
|
+
}
|
|
309
|
+
expect(threw).toBeTrue();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
export default tap.start();
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { IRustBridgeLogger } from './interfaces/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Buffer-based newline scanner for streaming binary data.
|
|
5
|
+
* Accumulates chunks and emits complete lines via callback.
|
|
6
|
+
* Used by both StdioTransport and SocketTransport.
|
|
7
|
+
*/
|
|
8
|
+
export class LineScanner {
|
|
9
|
+
private buffer: Buffer = Buffer.alloc(0);
|
|
10
|
+
private maxPayloadSize: number;
|
|
11
|
+
private logger: IRustBridgeLogger;
|
|
12
|
+
|
|
13
|
+
constructor(maxPayloadSize: number, logger: IRustBridgeLogger) {
|
|
14
|
+
this.maxPayloadSize = maxPayloadSize;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Feed a chunk of data. Calls onLine for each complete newline-terminated line found.
|
|
20
|
+
*/
|
|
21
|
+
public push(chunk: Buffer, onLine: (line: string) => void): void {
|
|
22
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
23
|
+
|
|
24
|
+
let newlineIndex: number;
|
|
25
|
+
while ((newlineIndex = this.buffer.indexOf(0x0A)) !== -1) {
|
|
26
|
+
const lineBuffer = this.buffer.subarray(0, newlineIndex);
|
|
27
|
+
this.buffer = this.buffer.subarray(newlineIndex + 1);
|
|
28
|
+
|
|
29
|
+
if (lineBuffer.length > this.maxPayloadSize) {
|
|
30
|
+
this.logger.log('error', `Inbound message exceeds maxPayloadSize (${lineBuffer.length} bytes), dropping`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const line = lineBuffer.toString('utf8').trim();
|
|
35
|
+
if (line) {
|
|
36
|
+
onLine(line);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Prevent OOM if sender never sends newline
|
|
41
|
+
if (this.buffer.length > this.maxPayloadSize) {
|
|
42
|
+
this.logger.log('error', `Buffer exceeded maxPayloadSize (${this.buffer.length} bytes) without newline, clearing`);
|
|
43
|
+
this.buffer = Buffer.alloc(0);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Reset the internal buffer. */
|
|
48
|
+
public clear(): void {
|
|
49
|
+
this.buffer = Buffer.alloc(0);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -124,7 +124,16 @@ export class RustBinaryLocator {
|
|
|
124
124
|
await plugins.fs.promises.access(filePath, plugins.fs.constants.X_OK);
|
|
125
125
|
return true;
|
|
126
126
|
} catch {
|
|
127
|
-
|
|
127
|
+
// File may exist but lack execute bit (common after npm/pnpm install).
|
|
128
|
+
// Try to make it executable.
|
|
129
|
+
try {
|
|
130
|
+
await plugins.fs.promises.access(filePath, plugins.fs.constants.F_OK);
|
|
131
|
+
await plugins.fs.promises.chmod(filePath, 0o755);
|
|
132
|
+
this.logger.log('info', `Auto-fixed missing execute permission on: ${filePath}`);
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
128
137
|
}
|
|
129
138
|
}
|
|
130
139
|
|