tywrap 0.2.0 → 0.2.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.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +20 -4
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +22 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/runtime/base.d.ts +17 -7
  7. package/dist/runtime/base.d.ts.map +1 -1
  8. package/dist/runtime/base.js +18 -1
  9. package/dist/runtime/base.js.map +1 -1
  10. package/dist/runtime/bounded-context.d.ts +252 -0
  11. package/dist/runtime/bounded-context.d.ts.map +1 -0
  12. package/dist/runtime/bounded-context.js +454 -0
  13. package/dist/runtime/bounded-context.js.map +1 -0
  14. package/dist/runtime/bridge-protocol.d.ts +167 -0
  15. package/dist/runtime/bridge-protocol.d.ts.map +1 -0
  16. package/dist/runtime/bridge-protocol.js +247 -0
  17. package/dist/runtime/bridge-protocol.js.map +1 -0
  18. package/dist/runtime/disposable.d.ts +40 -0
  19. package/dist/runtime/disposable.d.ts.map +1 -0
  20. package/dist/runtime/disposable.js +49 -0
  21. package/dist/runtime/disposable.js.map +1 -0
  22. package/dist/runtime/http-io.d.ts +91 -0
  23. package/dist/runtime/http-io.d.ts.map +1 -0
  24. package/dist/runtime/http-io.js +195 -0
  25. package/dist/runtime/http-io.js.map +1 -0
  26. package/dist/runtime/http.d.ts +47 -13
  27. package/dist/runtime/http.d.ts.map +1 -1
  28. package/dist/runtime/http.js +55 -74
  29. package/dist/runtime/http.js.map +1 -1
  30. package/dist/runtime/node.d.ts +97 -130
  31. package/dist/runtime/node.d.ts.map +1 -1
  32. package/dist/runtime/node.js +256 -523
  33. package/dist/runtime/node.js.map +1 -1
  34. package/dist/runtime/pooled-transport.d.ts +131 -0
  35. package/dist/runtime/pooled-transport.d.ts.map +1 -0
  36. package/dist/runtime/pooled-transport.js +175 -0
  37. package/dist/runtime/pooled-transport.js.map +1 -0
  38. package/dist/runtime/process-io.d.ts +204 -0
  39. package/dist/runtime/process-io.d.ts.map +1 -0
  40. package/dist/runtime/process-io.js +695 -0
  41. package/dist/runtime/process-io.js.map +1 -0
  42. package/dist/runtime/pyodide-io.d.ts +155 -0
  43. package/dist/runtime/pyodide-io.d.ts.map +1 -0
  44. package/dist/runtime/pyodide-io.js +397 -0
  45. package/dist/runtime/pyodide-io.js.map +1 -0
  46. package/dist/runtime/pyodide.d.ts +51 -19
  47. package/dist/runtime/pyodide.d.ts.map +1 -1
  48. package/dist/runtime/pyodide.js +57 -186
  49. package/dist/runtime/pyodide.js.map +1 -1
  50. package/dist/runtime/safe-codec.d.ts +81 -0
  51. package/dist/runtime/safe-codec.d.ts.map +1 -0
  52. package/dist/runtime/safe-codec.js +345 -0
  53. package/dist/runtime/safe-codec.js.map +1 -0
  54. package/dist/runtime/transport.d.ts +186 -0
  55. package/dist/runtime/transport.d.ts.map +1 -0
  56. package/dist/runtime/transport.js +86 -0
  57. package/dist/runtime/transport.js.map +1 -0
  58. package/dist/runtime/validators.d.ts +131 -0
  59. package/dist/runtime/validators.d.ts.map +1 -0
  60. package/dist/runtime/validators.js +219 -0
  61. package/dist/runtime/validators.js.map +1 -0
  62. package/dist/runtime/worker-pool.d.ts +196 -0
  63. package/dist/runtime/worker-pool.d.ts.map +1 -0
  64. package/dist/runtime/worker-pool.js +371 -0
  65. package/dist/runtime/worker-pool.js.map +1 -0
  66. package/dist/utils/codec.d.ts.map +1 -1
  67. package/dist/utils/codec.js +120 -1
  68. package/dist/utils/codec.js.map +1 -1
  69. package/package.json +2 -2
  70. package/runtime/python_bridge.py +30 -3
  71. package/runtime/safe_codec.py +344 -0
  72. package/src/index.ts +48 -5
  73. package/src/runtime/base.ts +18 -26
  74. package/src/runtime/bounded-context.ts +608 -0
  75. package/src/runtime/bridge-protocol.ts +319 -0
  76. package/src/runtime/disposable.ts +65 -0
  77. package/src/runtime/http-io.ts +244 -0
  78. package/src/runtime/http.ts +71 -117
  79. package/src/runtime/node.ts +358 -691
  80. package/src/runtime/pooled-transport.ts +252 -0
  81. package/src/runtime/process-io.ts +902 -0
  82. package/src/runtime/pyodide-io.ts +485 -0
  83. package/src/runtime/pyodide.ts +75 -215
  84. package/src/runtime/safe-codec.ts +443 -0
  85. package/src/runtime/transport.ts +273 -0
  86. package/src/runtime/validators.ts +241 -0
  87. package/src/runtime/worker-pool.ts +498 -0
  88. package/src/utils/codec.ts +126 -1
@@ -0,0 +1,273 @@
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({ id: '1', type: 'call', module: 'math', functionName: 'sqrt', args: [16] }),
126
+ * 5000
127
+ * );
128
+ *
129
+ * await transport.dispose();
130
+ * ```
131
+ */
132
+ export interface Transport extends Disposable {
133
+ /**
134
+ * Initialize the transport.
135
+ *
136
+ * This method establishes the underlying connection (e.g., spawns a process,
137
+ * opens a connection). It must be called before `send()` can be used.
138
+ *
139
+ * This method should be idempotent - calling it multiple times after
140
+ * successful initialization should be a no-op.
141
+ *
142
+ * @throws BridgeError if initialization fails
143
+ */
144
+ init(): Promise<void>;
145
+
146
+ /**
147
+ * Send a message and wait for the response.
148
+ *
149
+ * @param message - The JSON-encoded protocol message to send
150
+ * @param timeoutMs - Timeout in milliseconds (0 = no timeout)
151
+ * @param signal - Optional AbortSignal for external cancellation
152
+ * @returns The raw response string (JSON-encoded ProtocolResponse)
153
+ *
154
+ * @throws BridgeTimeoutError if the operation times out or is aborted
155
+ * @throws BridgeDisposedError if the transport has been disposed
156
+ * @throws BridgeProtocolError if the message format is invalid
157
+ * @throws BridgeError for other transport-level failures
158
+ */
159
+ send(message: string, timeoutMs: number, signal?: AbortSignal): Promise<string>;
160
+
161
+ /**
162
+ * Whether the transport is ready to send messages.
163
+ *
164
+ * Returns `true` after successful `init()` and before `dispose()`.
165
+ * Returns `false` in all other states.
166
+ */
167
+ readonly isReady: boolean;
168
+ }
169
+
170
+ // =============================================================================
171
+ // TRANSPORT OPTIONS
172
+ // =============================================================================
173
+
174
+ /**
175
+ * Base options for creating a Transport.
176
+ *
177
+ * Concrete transport implementations may extend this with
178
+ * additional configuration options.
179
+ */
180
+ export interface TransportOptions {
181
+ /**
182
+ * Default timeout for operations in milliseconds.
183
+ *
184
+ * This timeout applies to individual `send()` calls when
185
+ * no explicit timeout is provided.
186
+ *
187
+ * @default 30000 (30 seconds)
188
+ */
189
+ defaultTimeoutMs?: number;
190
+ }
191
+
192
+ // =============================================================================
193
+ // TYPE GUARDS
194
+ // =============================================================================
195
+
196
+ /**
197
+ * Type guard to check if a value implements the Transport interface.
198
+ *
199
+ * @param value - The value to check
200
+ * @returns True if the value has all required Transport methods and properties
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * function useTransport(maybeTransport: unknown) {
205
+ * if (isTransport(maybeTransport)) {
206
+ * await maybeTransport.init();
207
+ * // TypeScript now knows maybeTransport is Transport
208
+ * }
209
+ * }
210
+ * ```
211
+ */
212
+ export function isTransport(value: unknown): value is Transport {
213
+ return (
214
+ value !== null &&
215
+ typeof value === 'object' &&
216
+ typeof (value as Transport).init === 'function' &&
217
+ typeof (value as Transport).send === 'function' &&
218
+ typeof (value as Transport).dispose === 'function' &&
219
+ 'isReady' in value
220
+ );
221
+ }
222
+
223
+ /**
224
+ * Type guard to check if a value is a valid ProtocolMessage.
225
+ *
226
+ * @param value - The value to check
227
+ * @returns True if the value conforms to the ProtocolMessage structure
228
+ */
229
+ export function isProtocolMessage(value: unknown): value is ProtocolMessage {
230
+ if (value === null || typeof value !== 'object') {
231
+ return false;
232
+ }
233
+
234
+ const msg = value as ProtocolMessage;
235
+
236
+ return (
237
+ typeof msg.id === 'number' &&
238
+ msg.protocol === PROTOCOL_ID &&
239
+ typeof msg.method === 'string' &&
240
+ ['call', 'instantiate', 'call_method', 'dispose_instance', 'meta'].includes(msg.method) &&
241
+ typeof msg.params === 'object' &&
242
+ msg.params !== null
243
+ );
244
+ }
245
+
246
+ /**
247
+ * Type guard to check if a value is a valid ProtocolResponse.
248
+ *
249
+ * @param value - The value to check
250
+ * @returns True if the value conforms to the ProtocolResponse structure
251
+ */
252
+ export function isProtocolResponse(value: unknown): value is ProtocolResponse {
253
+ if (value === null || typeof value !== 'object') {
254
+ return false;
255
+ }
256
+
257
+ const resp = value as ProtocolResponse;
258
+
259
+ if (typeof resp.id !== 'number') {
260
+ return false;
261
+ }
262
+
263
+ // Must have either result or error (or neither for void returns)
264
+ if (resp.error !== undefined) {
265
+ if (typeof resp.error !== 'object' || resp.error === null) {
266
+ return false;
267
+ }
268
+ const err = resp.error;
269
+ return typeof err.type === 'string' && typeof err.message === 'string';
270
+ }
271
+
272
+ return true;
273
+ }
@@ -0,0 +1,241 @@
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
+ export function containsSpecialFloat(value: unknown): boolean {
187
+ if (typeof value === 'number') {
188
+ return !Number.isFinite(value);
189
+ }
190
+ if (Array.isArray(value)) {
191
+ return value.some(containsSpecialFloat);
192
+ }
193
+ if (isPlainObject(value)) {
194
+ return Object.values(value).some(containsSpecialFloat);
195
+ }
196
+ return false;
197
+ }
198
+
199
+ /**
200
+ * Assert that a value does not contain non-finite numbers.
201
+ *
202
+ * @param value - The value to check
203
+ * @param name - The name of the parameter (for error messages)
204
+ * @throws ValidationError if the value contains NaN or Infinity
205
+ */
206
+ export function assertNoSpecialFloats(value: unknown, name: string): void {
207
+ if (containsSpecialFloat(value)) {
208
+ throw new ValidationError(
209
+ `${name} contains non-finite numbers (NaN or Infinity) which cannot be serialized to JSON`
210
+ );
211
+ }
212
+ }
213
+
214
+ // ═══════════════════════════════════════════════════════════════════════════
215
+ // PATH VALIDATION (for CLI security)
216
+ // ═══════════════════════════════════════════════════════════════════════════
217
+
218
+ /**
219
+ * Sanitize a string for use in a cache key filename.
220
+ * Removes or replaces characters that could cause path traversal or invalid filenames.
221
+ *
222
+ * @param value - The string to sanitize
223
+ * @returns A safe string for use in filenames
224
+ */
225
+ export function sanitizeForFilename(value: string): string {
226
+ // Replace path separators and traversal patterns
227
+ return value
228
+ .replace(/\.\./g, '__')
229
+ .replace(/[/\\:*?"<>|]/g, '_')
230
+ .replace(/\s+/g, '_');
231
+ }
232
+
233
+ /**
234
+ * Check if a string looks like a path traversal attempt.
235
+ *
236
+ * @param value - The string to check
237
+ * @returns True if the string contains path traversal patterns
238
+ */
239
+ export function containsPathTraversal(value: string): boolean {
240
+ return value.includes('..') || value.includes('/') || value.includes('\\');
241
+ }