tywrap 0.2.0 → 0.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/README.md +65 -31
- package/dist/cli.js +49 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts +11 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +43 -7
- package/dist/config/index.js.map +1 -1
- package/dist/core/analyzer.d.ts +1 -0
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +47 -12
- package/dist/core/analyzer.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +8 -0
- package/dist/core/annotation-parser.d.ts.map +1 -0
- package/dist/core/annotation-parser.js +409 -0
- package/dist/core/annotation-parser.js.map +1 -0
- package/dist/core/discovery.d.ts +1 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +9 -9
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/generator.d.ts +9 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +738 -104
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +9 -7
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +129 -44
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/runtime/base.d.ts +17 -7
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +18 -1
- package/dist/runtime/base.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +252 -0
- package/dist/runtime/bounded-context.d.ts.map +1 -0
- package/dist/runtime/bounded-context.js +454 -0
- package/dist/runtime/bounded-context.js.map +1 -0
- package/dist/runtime/bridge-core.d.ts +1 -0
- package/dist/runtime/bridge-core.d.ts.map +1 -1
- package/dist/runtime/bridge-core.js +52 -17
- package/dist/runtime/bridge-core.js.map +1 -1
- package/dist/runtime/bridge-protocol.d.ts +184 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -0
- package/dist/runtime/bridge-protocol.js +344 -0
- package/dist/runtime/bridge-protocol.js.map +1 -0
- package/dist/runtime/disposable.d.ts +40 -0
- package/dist/runtime/disposable.d.ts.map +1 -0
- package/dist/runtime/disposable.js +49 -0
- package/dist/runtime/disposable.js.map +1 -0
- package/dist/runtime/errors.d.ts +10 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +9 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-io.d.ts +91 -0
- package/dist/runtime/http-io.d.ts.map +1 -0
- package/dist/runtime/http-io.js +195 -0
- package/dist/runtime/http-io.js.map +1 -0
- package/dist/runtime/http.d.ts +48 -13
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +60 -73
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts +97 -130
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +392 -521
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +1 -1
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +1 -1
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +135 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -0
- package/dist/runtime/pooled-transport.js +177 -0
- package/dist/runtime/pooled-transport.js.map +1 -0
- package/dist/runtime/process-io.d.ts +209 -0
- package/dist/runtime/process-io.d.ts.map +1 -0
- package/dist/runtime/process-io.js +729 -0
- package/dist/runtime/process-io.js.map +1 -0
- package/dist/runtime/pyodide-io.d.ts +154 -0
- package/dist/runtime/pyodide-io.d.ts.map +1 -0
- package/dist/runtime/pyodide-io.js +459 -0
- package/dist/runtime/pyodide-io.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +51 -19
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +57 -186
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +103 -0
- package/dist/runtime/safe-codec.d.ts.map +1 -0
- package/dist/runtime/safe-codec.js +519 -0
- package/dist/runtime/safe-codec.js.map +1 -0
- package/dist/runtime/transport.d.ts +191 -0
- package/dist/runtime/transport.d.ts.map +1 -0
- package/dist/runtime/transport.js +86 -0
- package/dist/runtime/transport.js.map +1 -0
- package/dist/runtime/validators.d.ts +120 -0
- package/dist/runtime/validators.d.ts.map +1 -0
- package/dist/runtime/validators.js +229 -0
- package/dist/runtime/validators.js.map +1 -0
- package/dist/runtime/worker-pool.d.ts +208 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -0
- package/dist/runtime/worker-pool.js +419 -0
- package/dist/runtime/worker-pool.js.map +1 -0
- package/dist/types/index.d.ts +51 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +227 -328
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +53 -5
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +114 -1
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +28 -0
- package/dist/utils/ir-cache.d.ts.map +1 -0
- package/dist/utils/ir-cache.js +29 -0
- package/dist/utils/ir-cache.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +1 -1
- package/dist/utils/parallel-processor.d.ts.map +1 -1
- package/dist/utils/parallel-processor.js +111 -40
- package/dist/utils/parallel-processor.js.map +1 -1
- package/dist/utils/runtime.d.ts +5 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +112 -18
- package/dist/utils/runtime.js.map +1 -1
- package/package.json +40 -18
- package/runtime/python_bridge.py +108 -15
- package/runtime/safe_codec.py +344 -0
- package/src/cli.ts +61 -2
- package/src/config/index.ts +63 -17
- package/src/core/analyzer.ts +52 -15
- package/src/core/annotation-parser.ts +500 -0
- package/src/core/discovery.ts +26 -12
- package/src/core/generator.ts +956 -116
- package/src/core/mapper.ts +158 -51
- package/src/index.ts +78 -6
- package/src/runtime/base.ts +18 -26
- package/src/runtime/bounded-context.ts +608 -0
- package/src/runtime/bridge-core.ts +58 -18
- package/src/runtime/bridge-protocol.ts +483 -0
- package/src/runtime/disposable.ts +65 -0
- package/src/runtime/errors.ts +14 -0
- package/src/runtime/http-io.ts +245 -0
- package/src/runtime/http.ts +76 -115
- package/src/runtime/node.ts +563 -676
- package/src/runtime/optimized-node.ts +4 -1
- package/src/runtime/pooled-transport.ts +263 -0
- package/src/runtime/process-io.ts +930 -0
- package/src/runtime/pyodide-io.ts +551 -0
- package/src/runtime/pyodide.ts +75 -215
- package/src/runtime/safe-codec.ts +651 -0
- package/src/runtime/transport.ts +278 -0
- package/src/runtime/validators.ts +252 -0
- package/src/runtime/worker-pool.ts +559 -0
- package/src/types/global.d.ts +11 -1
- package/src/types/index.ts +63 -0
- package/src/tywrap.ts +279 -358
- package/src/utils/cache.ts +59 -5
- package/src/utils/codec.ts +122 -1
- package/src/utils/ir-cache.ts +51 -0
- package/src/utils/parallel-processor.ts +119 -43
- package/src/utils/runtime.ts +129 -18
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport layer for BridgeProtocol.
|
|
3
|
+
*
|
|
4
|
+
* Provides an abstract I/O channel for all bridge communications across
|
|
5
|
+
* the JS-Python boundary. Concrete implementations handle different runtimes:
|
|
6
|
+
* - ProcessIO: Child process with stdio streams (Node.js)
|
|
7
|
+
* - HttpIO: HTTP POST requests (remote Python server)
|
|
8
|
+
* - PyodideIO: In-memory Pyodide calls (browser/WASM)
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/bbopen/tywrap/issues/149
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Disposable } from './disposable.js';
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// PROTOCOL CONSTANTS
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
/** Protocol identifier for tywrap communication */
|
|
20
|
+
export const PROTOCOL_ID = 'tywrap/1';
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// PROTOCOL TYPES
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Protocol message format for all transports.
|
|
28
|
+
*
|
|
29
|
+
* Each method corresponds to a BridgeProtocol operation:
|
|
30
|
+
* - `call`: Invoke a module-level function
|
|
31
|
+
* - `instantiate`: Create a new class instance
|
|
32
|
+
* - `call_method`: Invoke a method on an existing instance
|
|
33
|
+
* - `dispose_instance`: Release an instance handle
|
|
34
|
+
* - `meta`: Get bridge metadata
|
|
35
|
+
*/
|
|
36
|
+
export interface ProtocolMessage {
|
|
37
|
+
/** Unique message identifier for request-response correlation */
|
|
38
|
+
id: number;
|
|
39
|
+
|
|
40
|
+
/** Protocol identifier (must be 'tywrap/1') */
|
|
41
|
+
protocol: typeof PROTOCOL_ID;
|
|
42
|
+
|
|
43
|
+
/** The method to invoke */
|
|
44
|
+
method: 'call' | 'instantiate' | 'call_method' | 'dispose_instance' | 'meta';
|
|
45
|
+
|
|
46
|
+
/** Method parameters */
|
|
47
|
+
params: {
|
|
48
|
+
/** Python module path (for call and instantiate) */
|
|
49
|
+
module?: string;
|
|
50
|
+
|
|
51
|
+
/** Function name (for call) */
|
|
52
|
+
functionName?: string;
|
|
53
|
+
|
|
54
|
+
/** Class name (for instantiate) */
|
|
55
|
+
className?: string;
|
|
56
|
+
|
|
57
|
+
/** Instance handle (for call_method and dispose_instance) */
|
|
58
|
+
handle?: string;
|
|
59
|
+
|
|
60
|
+
/** Method name (for call_method) */
|
|
61
|
+
methodName?: string;
|
|
62
|
+
|
|
63
|
+
/** Positional arguments */
|
|
64
|
+
args?: unknown[];
|
|
65
|
+
|
|
66
|
+
/** Keyword arguments */
|
|
67
|
+
kwargs?: Record<string, unknown>;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Protocol response format from the Python side.
|
|
73
|
+
*
|
|
74
|
+
* A response contains either a result or an error, never both.
|
|
75
|
+
* The `id` field correlates the response to its originating request.
|
|
76
|
+
*/
|
|
77
|
+
export interface ProtocolResponse {
|
|
78
|
+
/** Message identifier matching the originating request */
|
|
79
|
+
id: number;
|
|
80
|
+
|
|
81
|
+
/** Protocol identifier (echoed back from request) */
|
|
82
|
+
protocol?: string;
|
|
83
|
+
|
|
84
|
+
/** Successful result value (undefined if error occurred) */
|
|
85
|
+
result?: unknown;
|
|
86
|
+
|
|
87
|
+
/** Error information (undefined if operation succeeded) */
|
|
88
|
+
error?: {
|
|
89
|
+
/** Python exception type name */
|
|
90
|
+
type: string;
|
|
91
|
+
/** Error message */
|
|
92
|
+
message: string;
|
|
93
|
+
/** Optional Python traceback for debugging */
|
|
94
|
+
traceback?: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// TRANSPORT INTERFACE
|
|
100
|
+
// =============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Abstract transport for sending messages across the JS-Python boundary.
|
|
104
|
+
*
|
|
105
|
+
* Transport implementations handle the low-level I/O details while
|
|
106
|
+
* providing a consistent interface for the higher-level BridgeProtocol.
|
|
107
|
+
*
|
|
108
|
+
* Lifecycle:
|
|
109
|
+
* 1. Create transport instance
|
|
110
|
+
* 2. Call `init()` to establish the connection
|
|
111
|
+
* 3. Use `send()` to exchange messages
|
|
112
|
+
* 4. Call `dispose()` to release resources
|
|
113
|
+
*
|
|
114
|
+
* Implementations:
|
|
115
|
+
* - ProcessIO: Spawns a Python child process, communicates via stdio
|
|
116
|
+
* - HttpIO: Sends HTTP POST requests to a Python server
|
|
117
|
+
* - PyodideIO: Calls Pyodide directly in-memory (WASM)
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const transport = new ProcessIO({ pythonPath: 'python3' });
|
|
122
|
+
* await transport.init();
|
|
123
|
+
*
|
|
124
|
+
* const response = await transport.send(
|
|
125
|
+
* JSON.stringify({
|
|
126
|
+
* id: 1,
|
|
127
|
+
* protocol: 'tywrap/1',
|
|
128
|
+
* method: 'call',
|
|
129
|
+
* params: { module: 'math', functionName: 'sqrt', args: [16], kwargs: {} },
|
|
130
|
+
* }),
|
|
131
|
+
* 5000
|
|
132
|
+
* );
|
|
133
|
+
*
|
|
134
|
+
* await transport.dispose();
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export interface Transport extends Disposable {
|
|
138
|
+
/**
|
|
139
|
+
* Initialize the transport.
|
|
140
|
+
*
|
|
141
|
+
* This method establishes the underlying connection (e.g., spawns a process,
|
|
142
|
+
* opens a connection). It must be called before `send()` can be used.
|
|
143
|
+
*
|
|
144
|
+
* This method should be idempotent - calling it multiple times after
|
|
145
|
+
* successful initialization should be a no-op.
|
|
146
|
+
*
|
|
147
|
+
* @throws BridgeError if initialization fails
|
|
148
|
+
*/
|
|
149
|
+
init(): Promise<void>;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Send a message and wait for the response.
|
|
153
|
+
*
|
|
154
|
+
* @param message - The JSON-encoded protocol message to send
|
|
155
|
+
* @param timeoutMs - Timeout in milliseconds (0 = no timeout)
|
|
156
|
+
* @param signal - Optional AbortSignal for external cancellation
|
|
157
|
+
* @returns The raw response string (JSON-encoded ProtocolResponse)
|
|
158
|
+
*
|
|
159
|
+
* @throws BridgeTimeoutError if the operation times out or is aborted
|
|
160
|
+
* @throws BridgeDisposedError if the transport has been disposed
|
|
161
|
+
* @throws BridgeProtocolError if the message format is invalid
|
|
162
|
+
* @throws BridgeError for other transport-level failures
|
|
163
|
+
*/
|
|
164
|
+
send(message: string, timeoutMs: number, signal?: AbortSignal): Promise<string>;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Whether the transport is ready to send messages.
|
|
168
|
+
*
|
|
169
|
+
* Returns `true` after successful `init()` and before `dispose()`.
|
|
170
|
+
* Returns `false` in all other states.
|
|
171
|
+
*/
|
|
172
|
+
readonly isReady: boolean;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// =============================================================================
|
|
176
|
+
// TRANSPORT OPTIONS
|
|
177
|
+
// =============================================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Base options for creating a Transport.
|
|
181
|
+
*
|
|
182
|
+
* Concrete transport implementations may extend this with
|
|
183
|
+
* additional configuration options.
|
|
184
|
+
*/
|
|
185
|
+
export interface TransportOptions {
|
|
186
|
+
/**
|
|
187
|
+
* Default timeout for operations in milliseconds.
|
|
188
|
+
*
|
|
189
|
+
* This timeout applies to individual `send()` calls when
|
|
190
|
+
* no explicit timeout is provided.
|
|
191
|
+
*
|
|
192
|
+
* @default 30000 (30 seconds)
|
|
193
|
+
*/
|
|
194
|
+
defaultTimeoutMs?: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// =============================================================================
|
|
198
|
+
// TYPE GUARDS
|
|
199
|
+
// =============================================================================
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Type guard to check if a value implements the Transport interface.
|
|
203
|
+
*
|
|
204
|
+
* @param value - The value to check
|
|
205
|
+
* @returns True if the value has all required Transport methods and properties
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* function useTransport(maybeTransport: unknown) {
|
|
210
|
+
* if (isTransport(maybeTransport)) {
|
|
211
|
+
* await maybeTransport.init();
|
|
212
|
+
* // TypeScript now knows maybeTransport is Transport
|
|
213
|
+
* }
|
|
214
|
+
* }
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
export function isTransport(value: unknown): value is Transport {
|
|
218
|
+
return (
|
|
219
|
+
value !== null &&
|
|
220
|
+
typeof value === 'object' &&
|
|
221
|
+
typeof (value as Transport).init === 'function' &&
|
|
222
|
+
typeof (value as Transport).send === 'function' &&
|
|
223
|
+
typeof (value as Transport).dispose === 'function' &&
|
|
224
|
+
'isReady' in value
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Type guard to check if a value is a valid ProtocolMessage.
|
|
230
|
+
*
|
|
231
|
+
* @param value - The value to check
|
|
232
|
+
* @returns True if the value conforms to the ProtocolMessage structure
|
|
233
|
+
*/
|
|
234
|
+
export function isProtocolMessage(value: unknown): value is ProtocolMessage {
|
|
235
|
+
if (value === null || typeof value !== 'object') {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const msg = value as ProtocolMessage;
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
typeof msg.id === 'number' &&
|
|
243
|
+
msg.protocol === PROTOCOL_ID &&
|
|
244
|
+
typeof msg.method === 'string' &&
|
|
245
|
+
['call', 'instantiate', 'call_method', 'dispose_instance', 'meta'].includes(msg.method) &&
|
|
246
|
+
typeof msg.params === 'object' &&
|
|
247
|
+
msg.params !== null
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Type guard to check if a value is a valid ProtocolResponse.
|
|
253
|
+
*
|
|
254
|
+
* @param value - The value to check
|
|
255
|
+
* @returns True if the value conforms to the ProtocolResponse structure
|
|
256
|
+
*/
|
|
257
|
+
export function isProtocolResponse(value: unknown): value is ProtocolResponse {
|
|
258
|
+
if (value === null || typeof value !== 'object') {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const resp = value as ProtocolResponse;
|
|
263
|
+
|
|
264
|
+
if (typeof resp.id !== 'number') {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Must have either result or error (or neither for void returns)
|
|
269
|
+
if (resp.error !== undefined) {
|
|
270
|
+
if (typeof resp.error !== 'object' || resp.error === null) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
const err = resp.error;
|
|
274
|
+
return typeof err.type === 'string' && typeof err.message === 'string';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure validation functions for runtime value checking.
|
|
3
|
+
*
|
|
4
|
+
* These functions are used by BoundedContext and can also be used
|
|
5
|
+
* independently for validation in CLI tools, config loading, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Error thrown when validation fails.
|
|
10
|
+
*/
|
|
11
|
+
export class ValidationError extends Error {
|
|
12
|
+
constructor(message: string) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'ValidationError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
// TYPE GUARDS
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a value is a finite number (not NaN, not Infinity).
|
|
24
|
+
*/
|
|
25
|
+
export function isFiniteNumber(value: unknown): value is number {
|
|
26
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if a value is a positive finite number.
|
|
31
|
+
*/
|
|
32
|
+
export function isPositiveNumber(value: unknown): value is number {
|
|
33
|
+
return isFiniteNumber(value) && value > 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a value is a non-negative finite number.
|
|
38
|
+
*/
|
|
39
|
+
export function isNonNegativeNumber(value: unknown): value is number {
|
|
40
|
+
return isFiniteNumber(value) && value >= 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a value is a non-empty string.
|
|
45
|
+
*/
|
|
46
|
+
export function isNonEmptyString(value: unknown): value is string {
|
|
47
|
+
return typeof value === 'string' && value.length > 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a value is a plain object (not null, not array).
|
|
52
|
+
*/
|
|
53
|
+
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
54
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
58
|
+
// ASSERTIONS
|
|
59
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Assert that a value is a finite number.
|
|
63
|
+
*
|
|
64
|
+
* @param value - The value to check
|
|
65
|
+
* @param name - The name of the parameter (for error messages)
|
|
66
|
+
* @returns The value as a number
|
|
67
|
+
* @throws ValidationError if the value is not a finite number
|
|
68
|
+
*/
|
|
69
|
+
export function assertFiniteNumber(value: unknown, name: string): number {
|
|
70
|
+
if (!isFiniteNumber(value)) {
|
|
71
|
+
const actual = typeof value === 'number' ? String(value) : typeof value;
|
|
72
|
+
throw new ValidationError(`${name} must be a finite number, got: ${actual}`);
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Assert that a value is a positive number (> 0).
|
|
79
|
+
*
|
|
80
|
+
* @param value - The value to check
|
|
81
|
+
* @param name - The name of the parameter (for error messages)
|
|
82
|
+
* @returns The value as a number
|
|
83
|
+
* @throws ValidationError if the value is not a positive number
|
|
84
|
+
*/
|
|
85
|
+
export function assertPositive(value: unknown, name: string): number {
|
|
86
|
+
const num = assertFiniteNumber(value, name);
|
|
87
|
+
if (num <= 0) {
|
|
88
|
+
throw new ValidationError(`${name} must be positive, got: ${num}`);
|
|
89
|
+
}
|
|
90
|
+
return num;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Assert that a value is a non-negative number (>= 0).
|
|
95
|
+
*
|
|
96
|
+
* @param value - The value to check
|
|
97
|
+
* @param name - The name of the parameter (for error messages)
|
|
98
|
+
* @returns The value as a number
|
|
99
|
+
* @throws ValidationError if the value is not a non-negative number
|
|
100
|
+
*/
|
|
101
|
+
export function assertNonNegative(value: unknown, name: string): number {
|
|
102
|
+
const num = assertFiniteNumber(value, name);
|
|
103
|
+
if (num < 0) {
|
|
104
|
+
throw new ValidationError(`${name} must be non-negative, got: ${num}`);
|
|
105
|
+
}
|
|
106
|
+
return num;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Assert that a value is a string.
|
|
111
|
+
*
|
|
112
|
+
* @param value - The value to check
|
|
113
|
+
* @param name - The name of the parameter (for error messages)
|
|
114
|
+
* @returns The value as a string
|
|
115
|
+
* @throws ValidationError if the value is not a string
|
|
116
|
+
*/
|
|
117
|
+
export function assertString(value: unknown, name: string): string {
|
|
118
|
+
if (typeof value !== 'string') {
|
|
119
|
+
throw new ValidationError(`${name} must be a string, got: ${typeof value}`);
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Assert that a value is a non-empty string.
|
|
126
|
+
*
|
|
127
|
+
* @param value - The value to check
|
|
128
|
+
* @param name - The name of the parameter (for error messages)
|
|
129
|
+
* @returns The value as a string
|
|
130
|
+
* @throws ValidationError if the value is not a non-empty string
|
|
131
|
+
*/
|
|
132
|
+
export function assertNonEmptyString(value: unknown, name: string): string {
|
|
133
|
+
const str = assertString(value, name);
|
|
134
|
+
if (str.length === 0) {
|
|
135
|
+
throw new ValidationError(`${name} must not be empty`);
|
|
136
|
+
}
|
|
137
|
+
return str;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Assert that a value is an array.
|
|
142
|
+
*
|
|
143
|
+
* @param value - The value to check
|
|
144
|
+
* @param name - The name of the parameter (for error messages)
|
|
145
|
+
* @returns The value as an array
|
|
146
|
+
* @throws ValidationError if the value is not an array
|
|
147
|
+
*/
|
|
148
|
+
export function assertArray<T = unknown>(value: unknown, name: string): T[] {
|
|
149
|
+
if (!Array.isArray(value)) {
|
|
150
|
+
throw new ValidationError(`${name} must be an array, got: ${typeof value}`);
|
|
151
|
+
}
|
|
152
|
+
return value as T[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Assert that a value is a plain object (not null, not array).
|
|
157
|
+
*
|
|
158
|
+
* @param value - The value to check
|
|
159
|
+
* @param name - The name of the parameter (for error messages)
|
|
160
|
+
* @returns The value as a Record
|
|
161
|
+
* @throws ValidationError if the value is not a plain object
|
|
162
|
+
*/
|
|
163
|
+
export function assertObject(value: unknown, name: string): Record<string, unknown> {
|
|
164
|
+
if (!isPlainObject(value)) {
|
|
165
|
+
const actual = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
|
|
166
|
+
throw new ValidationError(`${name} must be an object, got: ${actual}`);
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
172
|
+
// SPECIAL FLOAT DETECTION (for codec validation)
|
|
173
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check if a value contains non-finite numbers (NaN, Infinity, -Infinity).
|
|
177
|
+
* Recursively checks arrays and objects.
|
|
178
|
+
*
|
|
179
|
+
* This is used to detect values that cannot be safely serialized to JSON,
|
|
180
|
+
* as JSON.stringify converts NaN/Infinity to null, which can cause
|
|
181
|
+
* silent data corruption.
|
|
182
|
+
*
|
|
183
|
+
* @param value - The value to check
|
|
184
|
+
* @returns True if the value contains NaN or Infinity anywhere
|
|
185
|
+
*/
|
|
186
|
+
function containsSpecialFloatRecursive(value: unknown, visited: WeakSet<object>): boolean {
|
|
187
|
+
if (typeof value === 'number') {
|
|
188
|
+
return !Number.isFinite(value);
|
|
189
|
+
}
|
|
190
|
+
if (value === null || typeof value !== 'object') {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (visited.has(value)) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
visited.add(value);
|
|
197
|
+
if (Array.isArray(value)) {
|
|
198
|
+
return value.some(item => containsSpecialFloatRecursive(item, visited));
|
|
199
|
+
}
|
|
200
|
+
if (isPlainObject(value)) {
|
|
201
|
+
return Object.values(value).some(item => containsSpecialFloatRecursive(item, visited));
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function containsSpecialFloat(value: unknown): boolean {
|
|
207
|
+
return containsSpecialFloatRecursive(value, new WeakSet<object>());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Assert that a value does not contain non-finite numbers.
|
|
212
|
+
*
|
|
213
|
+
* @param value - The value to check
|
|
214
|
+
* @param name - The name of the parameter (for error messages)
|
|
215
|
+
* @throws ValidationError if the value contains NaN or Infinity
|
|
216
|
+
*/
|
|
217
|
+
export function assertNoSpecialFloats(value: unknown, name: string): void {
|
|
218
|
+
if (containsSpecialFloat(value)) {
|
|
219
|
+
throw new ValidationError(
|
|
220
|
+
`${name} contains non-finite numbers (NaN or Infinity) which cannot be serialized to JSON`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
226
|
+
// PATH VALIDATION (for CLI security)
|
|
227
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Sanitize a string for use in a cache key filename.
|
|
231
|
+
* Removes or replaces characters that could cause path traversal or invalid filenames.
|
|
232
|
+
*
|
|
233
|
+
* @param value - The string to sanitize
|
|
234
|
+
* @returns A safe string for use in filenames
|
|
235
|
+
*/
|
|
236
|
+
export function sanitizeForFilename(value: string): string {
|
|
237
|
+
// Replace path separators and traversal patterns
|
|
238
|
+
return value
|
|
239
|
+
.replace(/\.\./g, '__')
|
|
240
|
+
.replace(/[/\\:*?"<>|]/g, '_')
|
|
241
|
+
.replace(/\s+/g, '_');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check if a string looks like a path traversal attempt.
|
|
246
|
+
*
|
|
247
|
+
* @param value - The string to check
|
|
248
|
+
* @returns True if the string contains path traversal patterns
|
|
249
|
+
*/
|
|
250
|
+
export function containsPathTraversal(value: string): boolean {
|
|
251
|
+
return value.includes('..') || value.includes('/') || value.includes('\\');
|
|
252
|
+
}
|