@push.rocks/smartrust 2.5.0 → 2.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.
@@ -4,9 +4,16 @@ import type { IRustBridgeLogger } from './interfaces/index.js';
4
4
  * Buffer-based newline scanner for streaming binary data.
5
5
  * Accumulates chunks and emits complete lines via callback.
6
6
  * Used by both StdioTransport and SocketTransport.
7
+ *
8
+ * Each byte is scanned once and copied at most once into its line, so a line that arrives in many
9
+ * chunks costs time linear in its length.
7
10
  */
8
11
  export class LineScanner {
9
- private buffer: Buffer = Buffer.alloc(0);
12
+ /** Chunks of the line being received; none of them contains a newline. */
13
+ private pending: Buffer[] = [];
14
+ private pendingLength = 0;
15
+ /** Bytes after a line whose callback threw; they may contain newlines and are scanned first. */
16
+ private unscanned: Buffer = Buffer.alloc(0);
10
17
  private maxPayloadSize: number;
11
18
  private logger: IRustBridgeLogger;
12
19
 
@@ -19,44 +26,74 @@ export class LineScanner {
19
26
  * Feed a chunk of data. Calls onLine for each complete newline-terminated line found.
20
27
  */
21
28
  public push(chunk: Buffer, onLine: (line: string) => void): void {
22
- this.buffer = Buffer.concat([this.buffer, chunk]);
29
+ const input = this.unscanned.length > 0 ? Buffer.concat([this.unscanned, chunk]) : chunk;
30
+ this.unscanned = Buffer.alloc(0);
31
+ let start = 0;
23
32
 
24
33
  try {
25
34
  let newlineIndex: number;
26
- while ((newlineIndex = this.buffer.indexOf(0x0A)) !== -1) {
27
- const lineBuffer = this.buffer.subarray(0, newlineIndex);
28
- this.buffer = this.buffer.subarray(newlineIndex + 1);
29
-
30
- if (lineBuffer.length > this.maxPayloadSize) {
31
- this.logger.log('error', `Inbound message exceeds maxPayloadSize (${lineBuffer.length} bytes), dropping`);
35
+ while ((newlineIndex = input.indexOf(0x0A, start)) !== -1) {
36
+ const lineBuffer = this.takeLine(input.subarray(start, newlineIndex));
37
+ start = newlineIndex + 1;
38
+ if (!lineBuffer) {
32
39
  continue;
33
40
  }
34
-
35
41
  const line = lineBuffer.toString('utf8').trim();
36
42
  if (line) {
37
43
  onLine(line);
38
44
  }
39
45
  }
40
-
41
- // Prevent OOM if sender never sends newline
42
- if (this.buffer.length > this.maxPayloadSize) {
43
- this.logger.log('error', `Buffer exceeded maxPayloadSize (${this.buffer.length} bytes) without newline, clearing`);
44
- this.buffer = Buffer.alloc(0);
45
- }
46
- } finally {
47
- // A subarray retains its entire backing allocation, even when empty.
48
- // Compact once per chunk, including when a callback throws, without
49
- // repeatedly copying the unread suffix for each parsed line.
50
- if (this.buffer.length === 0) {
51
- this.buffer = Buffer.alloc(0);
52
- } else if (this.buffer.buffer.byteLength > Math.max(Buffer.poolSize, this.buffer.length * 2)) {
53
- this.buffer = Buffer.from(this.buffer);
54
- }
46
+ this.appendPartial(input.subarray(start));
47
+ } catch (error) {
48
+ // Keep what follows the failed line for the next push, without retaining what was consumed.
49
+ this.unscanned = LineScanner.compact(input.subarray(start));
50
+ throw error;
55
51
  }
56
52
  }
57
53
 
58
54
  /** Reset the internal buffer. */
59
55
  public clear(): void {
60
- this.buffer = Buffer.alloc(0);
56
+ this.pending = [];
57
+ this.pendingLength = 0;
58
+ this.unscanned = Buffer.alloc(0);
59
+ }
60
+
61
+ /** Joins the pending chunks with the line's last piece; null when the line is too large. */
62
+ private takeLine(tail: Buffer): Buffer | null {
63
+ const length = this.pendingLength + tail.length;
64
+ const pending = this.pending;
65
+ this.pending = [];
66
+ this.pendingLength = 0;
67
+ if (length > this.maxPayloadSize) {
68
+ this.logger.log('error', `Inbound message exceeds maxPayloadSize (${length} bytes), dropping`);
69
+ return null;
70
+ }
71
+ return pending.length === 0 ? tail : Buffer.concat([...pending, tail], length);
72
+ }
73
+
74
+ private appendPartial(partial: Buffer): void {
75
+ if (partial.length === 0) {
76
+ return;
77
+ }
78
+ // Prevent OOM if sender never sends newline
79
+ if (this.pendingLength + partial.length > this.maxPayloadSize) {
80
+ this.logger.log(
81
+ 'error',
82
+ `Buffer exceeded maxPayloadSize (${this.pendingLength + partial.length} bytes) without newline, clearing`,
83
+ );
84
+ this.pending = [];
85
+ this.pendingLength = 0;
86
+ return;
87
+ }
88
+ this.pending.push(LineScanner.compact(partial));
89
+ this.pendingLength += partial.length;
90
+ }
91
+
92
+ /** A subarray retains its entire backing allocation; copy a small piece of a large one. */
93
+ private static compact(piece: Buffer): Buffer {
94
+ if (piece.length === 0) {
95
+ return Buffer.alloc(0);
96
+ }
97
+ return piece.buffer.byteLength > Math.max(Buffer.poolSize, piece.length * 2) ? Buffer.from(piece) : piece;
61
98
  }
62
99
  }
@@ -10,9 +10,11 @@ const defaultLogger: IRustBridgeLogger = {
10
10
  * Locates a Rust binary using a priority-ordered search strategy:
11
11
  * 1. Explicit binaryPath override
12
12
  * 2. Environment variable
13
- * 3. Platform-specific npm package
14
- * 4. Local development build paths
15
- * 5. System PATH
13
+ * 3. Inside a Node.js single executable application: the sidecar embedded under `binaryName`,
14
+ * and nothing else
15
+ * 4. Platform-specific npm package
16
+ * 5. Local development build paths
17
+ * 6. System PATH
16
18
  */
17
19
  export class RustBinaryLocator {
18
20
  private options: IBinaryLocatorOptions;
@@ -37,11 +39,18 @@ export class RustBinaryLocator {
37
39
  * Find the binary path.
38
40
  * Returns null if discovery finds no binary. An unusable explicit path
39
41
  * rejects with RustBinaryLocatorError without trying discovery or chmod.
42
+ * Inside a single executable application, a sidecar it cannot provide rejects
43
+ * with RustBinaryLocatorError (`ERR_RUST_BINARY_SEA_SIDECAR_UNAVAILABLE`).
40
44
  */
41
45
  public async findBinary(): Promise<string | null> {
42
46
  if (this.options.binaryPath !== undefined) {
43
47
  return this.validateExplicitPath();
44
48
  }
49
+ // Inside a SEA every lookup resolves again: smartsea refreshes the extracted
50
+ // generation's lease and re-extracts a pruned tree, which a cached path would skip.
51
+ if (plugins.smartsea.isSea()) {
52
+ return this.findSeaBinary();
53
+ }
45
54
  if (this.cachedPath !== null) {
46
55
  if (await this.isExecutable(this.cachedPath)) return this.cachedPath;
47
56
  this.cachedPath = null;
@@ -62,18 +71,12 @@ export class RustBinaryLocator {
62
71
  const { binaryName } = this.options;
63
72
 
64
73
  // 2. Environment variable override
65
- if (this.options.envVarName) {
66
- const envPath = process.env[this.options.envVarName];
67
- if (envPath) {
68
- if (await this.isExecutable(envPath)) {
69
- this.logger.log('info', `Binary found via ${this.options.envVarName}: ${envPath}`);
70
- return envPath;
71
- }
72
- this.logger.log('warn', `${this.options.envVarName} set but not executable: ${envPath}`);
73
- }
74
+ const envPath = await this.findEnvOverride();
75
+ if (envPath) {
76
+ return envPath;
74
77
  }
75
78
 
76
- // 3. Platform-specific npm package
79
+ // 4. Platform-specific npm package
77
80
  if (this.options.platformPackagePrefix) {
78
81
  const platformBinary = await this.findPlatformPackageBinary();
79
82
  if (platformBinary) {
@@ -82,7 +85,7 @@ export class RustBinaryLocator {
82
85
  }
83
86
  }
84
87
 
85
- // 4. Local development build paths
88
+ // 5. Local development build paths
86
89
  const localPaths = this.options.localPaths || [
87
90
  plugins.path.resolve(process.cwd(), `rust/target/release/${binaryName}`),
88
91
  plugins.path.resolve(process.cwd(), `rust/target/debug/${binaryName}`),
@@ -101,7 +104,7 @@ export class RustBinaryLocator {
101
104
  }
102
105
  }
103
106
 
104
- // 5. System PATH
107
+ // 6. System PATH
105
108
  if (this.options.searchSystemPath !== false) {
106
109
  const systemPath = await this.findInPath(binaryName);
107
110
  if (systemPath) {
@@ -114,6 +117,49 @@ export class RustBinaryLocator {
114
117
  return null;
115
118
  }
116
119
 
120
+ private async findEnvOverride(): Promise<string | null> {
121
+ if (!this.options.envVarName) {
122
+ return null;
123
+ }
124
+ const envPath = process.env[this.options.envVarName];
125
+ if (!envPath) {
126
+ return null;
127
+ }
128
+ if (await this.isExecutable(envPath)) {
129
+ this.logger.log('info', `Binary found via ${this.options.envVarName}: ${envPath}`);
130
+ return envPath;
131
+ }
132
+ this.logger.log('warn', `${this.options.envVarName} set but not executable: ${envPath}`);
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Inside a single executable application: the environment override, else the sidecar the
138
+ * executable embeds under `binaryName`. Packages, local paths and PATH belong to the machine,
139
+ * not to the application, and are never searched.
140
+ */
141
+ private async findSeaBinary(): Promise<string> {
142
+ const envPath = await this.findEnvOverride();
143
+ if (envPath) {
144
+ return envPath;
145
+ }
146
+ const { binaryName } = this.options;
147
+ let sidecarPath: string;
148
+ try {
149
+ sidecarPath = await plugins.smartsea.getSidecarPath(binaryName);
150
+ } catch (error) {
151
+ const reason = error instanceof Error ? error.message : String(error);
152
+ throw new RustBinaryLocatorError(
153
+ `This single executable application cannot provide its embedded sidecar '${binaryName}': ${reason}`,
154
+ undefined,
155
+ error,
156
+ 'ERR_RUST_BINARY_SEA_SIDECAR_UNAVAILABLE',
157
+ );
158
+ }
159
+ this.logger.log('info', `Binary found as embedded sidecar: ${sidecarPath}`);
160
+ return sidecarPath;
161
+ }
162
+
117
163
  private async findPlatformPackageBinary(): Promise<string | null> {
118
164
  const { binaryName, platformPackagePrefix } = this.options;
119
165
  const platform = process.platform;
@@ -1,13 +1,30 @@
1
- /** Failure to use the caller's explicit executable selection. */
1
+ /**
2
+ * Why a binary lookup failed:
3
+ * - `ERR_RUST_BINARY_EXPLICIT_PATH_INVALID`: the caller's explicit `binaryPath` is unusable.
4
+ * - `ERR_RUST_BINARY_SEA_SIDECAR_UNAVAILABLE`: inside a Node.js single executable application,
5
+ * the sidecar embedded under the binary's name could not be provided (not embedded, no usable
6
+ * cache directory, extraction failed, …); `cause` carries the `SmartSeaError`.
7
+ */
8
+ export type TRustBinaryLocatorErrorCode =
9
+ | 'ERR_RUST_BINARY_EXPLICIT_PATH_INVALID'
10
+ | 'ERR_RUST_BINARY_SEA_SIDECAR_UNAVAILABLE';
11
+
12
+ /**
13
+ * A binary lookup that must not continue with other candidates: an unusable explicit
14
+ * `binaryPath`, or an embedded sidecar a single executable application cannot provide.
15
+ * `RustBridge.spawn()` rejects with it instead of resolving `false`.
16
+ */
2
17
  export class RustBinaryLocatorError extends Error {
3
- public readonly code = 'ERR_RUST_BINARY_EXPLICIT_PATH_INVALID' as const;
18
+ public readonly code: TRustBinaryLocatorErrorCode;
4
19
 
5
20
  constructor(
6
21
  message: string,
7
22
  public readonly binaryPath: string | undefined,
8
23
  cause?: unknown,
24
+ code: TRustBinaryLocatorErrorCode = 'ERR_RUST_BINARY_EXPLICIT_PATH_INVALID',
9
25
  ) {
10
26
  super(message, cause === undefined ? undefined : { cause });
11
27
  this.name = 'RustBinaryLocatorError';
28
+ this.code = code;
12
29
  }
13
30
  }
@@ -155,7 +155,8 @@ export class RustBridge<TCommands extends TCommandMap = TCommandMap> extends plu
155
155
  /**
156
156
  * Spawn the Rust binary and wait for it to signal readiness.
157
157
  * Returns true if the binary was found and spawned successfully.
158
- * Rejects with RustBinaryLocatorError for an invalid explicit binaryPath.
158
+ * Rejects with RustBinaryLocatorError for an invalid explicit binaryPath and,
159
+ * inside a single executable application, for an embedded sidecar it cannot provide.
159
160
  */
160
161
  public spawn(): Promise<boolean> {
161
162
  return this.activate(async (generationArg) => {
package/ts/index.ts CHANGED
@@ -7,7 +7,7 @@ export {
7
7
  type TRustBridgeRequestErrorCode,
8
8
  } from './classes.rustbridgerequesterror.js';
9
9
  export { RustBinaryLocator } from './classes.rustbinarylocator.js';
10
- export { RustBinaryLocatorError } from './classes.rustbinarylocatorerror.js';
10
+ export { RustBinaryLocatorError, type TRustBinaryLocatorErrorCode } from './classes.rustbinarylocatorerror.js';
11
11
  export { StreamingResponse } from './classes.streamingresponse.js';
12
12
  export { StdioTransport } from './classes.stdiotransport.js';
13
13
  export { SocketTransport } from './classes.sockettransport.js';
package/ts/plugins.ts CHANGED
@@ -11,5 +11,6 @@ export { path, fs, childProcess, events, url, net, stream };
11
11
 
12
12
  // @push.rocks scope
13
13
  import * as smartpath from '@push.rocks/smartpath';
14
+ import * as smartsea from '@push.rocks/smartsea';
14
15
 
15
- export { smartpath };
16
+ export { smartpath, smartsea };