tywrap 0.4.0 → 0.5.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 (89) hide show
  1. package/README.md +1 -0
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -6
  5. package/dist/index.js.map +1 -1
  6. package/dist/runtime/base.d.ts +7 -7
  7. package/dist/runtime/base.js +8 -8
  8. package/dist/runtime/bounded-context.d.ts +15 -31
  9. package/dist/runtime/bounded-context.d.ts.map +1 -1
  10. package/dist/runtime/bounded-context.js +16 -15
  11. package/dist/runtime/bounded-context.js.map +1 -1
  12. package/dist/runtime/http.d.ts +24 -5
  13. package/dist/runtime/http.d.ts.map +1 -1
  14. package/dist/runtime/http.js +57 -12
  15. package/dist/runtime/http.js.map +1 -1
  16. package/dist/runtime/node.d.ts +33 -7
  17. package/dist/runtime/node.d.ts.map +1 -1
  18. package/dist/runtime/node.js +139 -151
  19. package/dist/runtime/node.js.map +1 -1
  20. package/dist/runtime/pooled-transport.d.ts +2 -18
  21. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  22. package/dist/runtime/pooled-transport.js +2 -29
  23. package/dist/runtime/pooled-transport.js.map +1 -1
  24. package/dist/runtime/process-io.d.ts +2 -18
  25. package/dist/runtime/process-io.d.ts.map +1 -1
  26. package/dist/runtime/process-io.js +3 -30
  27. package/dist/runtime/process-io.js.map +1 -1
  28. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts +12 -0
  29. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -0
  30. package/dist/runtime/pyodide-bootstrap-core.generated.js +12 -0
  31. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -0
  32. package/dist/runtime/pyodide-io.d.ts +7 -37
  33. package/dist/runtime/pyodide-io.d.ts.map +1 -1
  34. package/dist/runtime/pyodide-io.js +90 -214
  35. package/dist/runtime/pyodide-io.js.map +1 -1
  36. package/dist/runtime/pyodide.d.ts +25 -5
  37. package/dist/runtime/pyodide.d.ts.map +1 -1
  38. package/dist/runtime/pyodide.js +59 -10
  39. package/dist/runtime/pyodide.js.map +1 -1
  40. package/dist/runtime/{bridge-protocol.d.ts → rpc-client.d.ts} +59 -46
  41. package/dist/runtime/rpc-client.d.ts.map +1 -0
  42. package/dist/runtime/{bridge-protocol.js → rpc-client.js} +60 -42
  43. package/dist/runtime/rpc-client.js.map +1 -0
  44. package/dist/runtime/worker-pool.d.ts +2 -19
  45. package/dist/runtime/worker-pool.d.ts.map +1 -1
  46. package/dist/runtime/worker-pool.js +2 -30
  47. package/dist/runtime/worker-pool.js.map +1 -1
  48. package/dist/types/index.d.ts +8 -3
  49. package/dist/types/index.d.ts.map +1 -1
  50. package/dist/tywrap.d.ts.map +1 -1
  51. package/dist/tywrap.js +1 -3
  52. package/dist/tywrap.js.map +1 -1
  53. package/package.json +9 -12
  54. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  55. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  56. package/runtime/python_bridge.py +85 -702
  57. package/runtime/safe_codec.py +10 -2
  58. package/runtime/tywrap_bridge_core.py +875 -0
  59. package/src/index.ts +6 -6
  60. package/src/runtime/base.ts +8 -8
  61. package/src/runtime/bounded-context.ts +17 -56
  62. package/src/runtime/http.ts +85 -13
  63. package/src/runtime/node.ts +165 -217
  64. package/src/runtime/pooled-transport.ts +2 -57
  65. package/src/runtime/process-io.ts +3 -55
  66. package/src/runtime/pyodide-bootstrap-core.generated.ts +12 -0
  67. package/src/runtime/pyodide-io.ts +92 -243
  68. package/src/runtime/pyodide.ts +87 -10
  69. package/src/runtime/{bridge-protocol.ts → rpc-client.ts} +76 -49
  70. package/src/runtime/worker-pool.ts +2 -58
  71. package/src/types/index.ts +20 -4
  72. package/src/tywrap.ts +1 -3
  73. package/dist/core/analyzer.d.ts +0 -64
  74. package/dist/core/analyzer.d.ts.map +0 -1
  75. package/dist/core/analyzer.js +0 -710
  76. package/dist/core/analyzer.js.map +0 -1
  77. package/dist/runtime/bridge-core.d.ts +0 -65
  78. package/dist/runtime/bridge-core.d.ts.map +0 -1
  79. package/dist/runtime/bridge-core.js +0 -379
  80. package/dist/runtime/bridge-core.js.map +0 -1
  81. package/dist/runtime/bridge-protocol.d.ts.map +0 -1
  82. package/dist/runtime/bridge-protocol.js.map +0 -1
  83. package/dist/utils/parallel-processor.d.ts +0 -146
  84. package/dist/utils/parallel-processor.d.ts.map +0 -1
  85. package/dist/utils/parallel-processor.js +0 -707
  86. package/dist/utils/parallel-processor.js.map +0 -1
  87. package/src/core/analyzer.ts +0 -824
  88. package/src/runtime/bridge-core.ts +0 -494
  89. package/src/utils/parallel-processor.ts +0 -955
@@ -1,494 +0,0 @@
1
- import type { BridgeInfo } from '../types/index.js';
2
-
3
- import { BridgeExecutionError, BridgeProtocolError, BridgeTimeoutError } from './errors.js';
4
- import { TYWRAP_PROTOCOL, TYWRAP_PROTOCOL_VERSION } from './protocol.js';
5
- import { TimedOutRequestTracker } from './timed-out-request-tracker.js';
6
-
7
- export type RpcMethod = 'call' | 'instantiate' | 'call_method' | 'dispose_instance' | 'meta';
8
-
9
- export interface RpcRequest {
10
- id: number;
11
- protocol: string;
12
- method: RpcMethod;
13
- params: unknown;
14
- }
15
-
16
- export interface RpcResponse<T = unknown> {
17
- id: number;
18
- protocol: string;
19
- result?: T;
20
- error?: { type: string; message: string; traceback?: string };
21
- }
22
-
23
- export interface BridgeCoreTransport {
24
- write: (data: string) => void;
25
- }
26
-
27
- export interface BridgeCoreOptions {
28
- timeoutMs: number;
29
- maxLineLength?: number;
30
- maxStderrBytes?: number;
31
- protocol?: string;
32
- protocolVersion?: number;
33
- decodeValue?: (value: unknown) => Promise<unknown>;
34
- onFatalError?: (error: BridgeProtocolError) => void;
35
- onTimeout?: (error: BridgeTimeoutError) => void;
36
- }
37
-
38
- interface PendingRequest {
39
- resolve: (value: unknown) => void;
40
- reject: (error: unknown) => void;
41
- timer?: NodeJS.Timeout;
42
- }
43
-
44
- // Why: keep stdout lines bounded to prevent unbounded memory growth on malformed output.
45
- const DEFAULT_MAX_LINE_LENGTH = 1024 * 1024; // 1MB
46
- // Why: keep stderr snapshots small but useful for diagnostics on timeouts/exits.
47
- const DEFAULT_MAX_STDERR_BYTES = 8 * 1024; // 8KB
48
-
49
- const defaultDecodeValue = async (value: unknown): Promise<unknown> => value;
50
- const noop = (): void => {};
51
- const ANSI_ESCAPE_RE = new RegExp('\\u001b\\[[0-9;]*[A-Za-z]', 'g');
52
- const CONTROL_CHARS_RE = new RegExp(
53
- '[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F\\u0080-\\u009F]',
54
- 'g'
55
- );
56
- const sanitizeStderr = (value: string): string =>
57
- value.replace(ANSI_ESCAPE_RE, '').replace(CONTROL_CHARS_RE, '');
58
-
59
- export class BridgeCore {
60
- private readonly transport: BridgeCoreTransport;
61
- private readonly options: Required<BridgeCoreOptions>;
62
- private readonly timedOutRequests: TimedOutRequestTracker;
63
- private readonly pending = new Map<number, PendingRequest>();
64
- private nextId = 1;
65
- private stdoutBuffer = '';
66
- private stderrBuffer = '';
67
- private protocolError = false;
68
-
69
- constructor(transport: BridgeCoreTransport, options: BridgeCoreOptions) {
70
- this.transport = transport;
71
- this.options = {
72
- timeoutMs: options.timeoutMs,
73
- maxLineLength: options.maxLineLength ?? DEFAULT_MAX_LINE_LENGTH,
74
- maxStderrBytes: options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES,
75
- protocol: options.protocol ?? TYWRAP_PROTOCOL,
76
- protocolVersion: options.protocolVersion ?? TYWRAP_PROTOCOL_VERSION,
77
- decodeValue: options.decodeValue ?? defaultDecodeValue,
78
- onFatalError: options.onFatalError ?? noop,
79
- onTimeout: options.onTimeout ?? noop,
80
- };
81
- this.timedOutRequests = new TimedOutRequestTracker({
82
- ttlMs: Math.max(1000, this.options.timeoutMs * 2),
83
- });
84
- }
85
-
86
- getStderrTail(): string {
87
- return this.stderrBuffer.trim();
88
- }
89
-
90
- getPendingCount(): number {
91
- return this.pending.size;
92
- }
93
-
94
- clear(): void {
95
- for (const [, pending] of this.pending) {
96
- if (pending.timer) {
97
- clearTimeout(pending.timer);
98
- }
99
- }
100
- this.pending.clear();
101
- this.timedOutRequests.clear();
102
- this.stdoutBuffer = '';
103
- this.stderrBuffer = '';
104
- this.protocolError = false;
105
- }
106
-
107
- async send<T>(payload: Omit<RpcRequest, 'id' | 'protocol'>): Promise<T> {
108
- const id = this.nextId++;
109
- const message: RpcRequest = { ...payload, id, protocol: this.options.protocol } as RpcRequest;
110
- let text: string;
111
- try {
112
- text = JSON.stringify(message);
113
- } catch (err) {
114
- throw new BridgeProtocolError(
115
- `Failed to serialize request: ${err instanceof Error ? err.message : String(err)}`
116
- );
117
- }
118
-
119
- let timer: NodeJS.Timeout | undefined;
120
- const promise = new Promise<T>((resolve, reject) => {
121
- timer = setTimeout(() => {
122
- this.pending.delete(id);
123
- this.timedOutRequests.mark(id);
124
- const stderrTail = this.getStderrTail();
125
- const msg = stderrTail
126
- ? `Python call timed out. Recent stderr from Python:\n${stderrTail}`
127
- : 'Python call timed out';
128
- const timeoutError = new BridgeTimeoutError(msg);
129
- reject(timeoutError);
130
- this.options.onTimeout(timeoutError);
131
- }, this.options.timeoutMs);
132
-
133
- const resolveWrapped = (value: unknown): void => {
134
- resolve(value as T);
135
- };
136
- this.pending.set(id, { resolve: resolveWrapped, reject, timer });
137
- });
138
-
139
- try {
140
- this.transport.write(`${text}\n`);
141
- } catch (err) {
142
- const failure = new BridgeProtocolError(
143
- `IPC failure: ${err instanceof Error ? err.message : String(err)}`
144
- );
145
- this.failRequest(id, failure);
146
- this.handleFatalError(failure);
147
- }
148
-
149
- return promise;
150
- }
151
-
152
- handleStdoutData(chunk: Buffer | string): void {
153
- this.stdoutBuffer += chunk.toString();
154
-
155
- if (
156
- this.stdoutBuffer.length > this.options.maxLineLength &&
157
- !this.stdoutBuffer.includes('\n')
158
- ) {
159
- const snippet = this.stdoutBuffer.slice(0, this.options.maxLineLength);
160
- this.stdoutBuffer = '';
161
- this.handleProtocolError(
162
- `Response line exceeded ${this.options.maxLineLength} bytes`,
163
- snippet
164
- );
165
- return;
166
- }
167
-
168
- let idx: number;
169
- while ((idx = this.stdoutBuffer.indexOf('\n')) !== -1) {
170
- const line = this.stdoutBuffer.slice(0, idx);
171
- this.stdoutBuffer = this.stdoutBuffer.slice(idx + 1);
172
-
173
- if (!line.trim()) {
174
- continue;
175
- }
176
-
177
- if (line.length > this.options.maxLineLength) {
178
- const snippet = line.slice(0, this.options.maxLineLength);
179
- this.handleProtocolError(
180
- `Response line exceeded ${this.options.maxLineLength} bytes`,
181
- snippet
182
- );
183
- return;
184
- }
185
-
186
- this.handleResponseLine(line);
187
- }
188
- }
189
-
190
- handleStderrData(chunk: Buffer | string): void {
191
- try {
192
- this.stderrBuffer += sanitizeStderr(chunk.toString());
193
- if (this.stderrBuffer.length > this.options.maxStderrBytes) {
194
- this.stderrBuffer = this.stderrBuffer.slice(
195
- this.stderrBuffer.length - this.options.maxStderrBytes
196
- );
197
- }
198
- } catch {
199
- // Ignore stderr buffering errors
200
- }
201
- }
202
-
203
- handleProcessExit(): void {
204
- const stderrTail = this.getStderrTail();
205
- const msg = stderrTail
206
- ? `Python process exited. Stderr:\n${stderrTail}`
207
- : 'Python process exited';
208
- this.rejectAll(new BridgeProtocolError(msg));
209
- }
210
-
211
- handleProcessError(err: Error): void {
212
- const msg = `Python process error: ${err.message}`;
213
- const error = new BridgeProtocolError(msg);
214
- this.rejectAll(error);
215
- this.handleFatalError(error);
216
- }
217
-
218
- private handleResponseLine(line: string): void {
219
- let msg: RpcResponse;
220
- try {
221
- const parsed = JSON.parse(line) as unknown;
222
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
223
- this.handleProtocolError('Invalid response payload', line);
224
- return;
225
- }
226
- msg = parsed as RpcResponse;
227
- } catch (err) {
228
- const parseMessage = err instanceof Error ? err.message : String(err);
229
- this.handleProtocolError(`Invalid JSON: ${parseMessage}`, line);
230
- return;
231
- }
232
-
233
- if (msg.protocol !== this.options.protocol) {
234
- this.handleProtocolError(
235
- `Invalid protocol. Expected ${this.options.protocol} but received ${String(msg.protocol)}`,
236
- line
237
- );
238
- return;
239
- }
240
-
241
- if (typeof msg.id !== 'number') {
242
- this.handleProtocolError('Invalid response id', line);
243
- return;
244
- }
245
-
246
- const pending = this.pending.get(msg.id);
247
- if (!pending) {
248
- if (this.timedOutRequests.consume(msg.id)) {
249
- return;
250
- }
251
- this.handleProtocolError(`Unexpected response id ${msg.id}`, line);
252
- return;
253
- }
254
-
255
- const hasResult = Object.prototype.hasOwnProperty.call(msg, 'result');
256
- const hasError = Object.prototype.hasOwnProperty.call(msg, 'error');
257
-
258
- if (hasResult && hasError) {
259
- this.handleProtocolError('Response cannot include both "result" and "error"', line);
260
- return;
261
- }
262
-
263
- if (hasError) {
264
- const errorPayload = this.normalizeErrorPayload(msg.error);
265
- if (!errorPayload) {
266
- const reason = this.describeInvalidErrorPayload(msg.error);
267
- this.handleProtocolError(`Invalid response "error" payload: ${reason}`, line);
268
- return;
269
- }
270
- this.pending.delete(msg.id);
271
- if (pending.timer) {
272
- clearTimeout(pending.timer);
273
- }
274
- pending.reject(this.errorFrom(errorPayload));
275
- return;
276
- }
277
-
278
- this.pending.delete(msg.id);
279
- if (pending.timer) {
280
- clearTimeout(pending.timer);
281
- }
282
-
283
- Promise.resolve(this.options.decodeValue(msg.result))
284
- .then(decoded => pending.resolve(decoded))
285
- .catch(err => {
286
- pending.reject(
287
- new BridgeProtocolError(
288
- `Failed to decode Python response: ${err instanceof Error ? err.message : String(err)}`
289
- )
290
- );
291
- });
292
- }
293
-
294
- private errorFrom(err: { type: string; message: string; traceback?: string }): Error {
295
- const error = new BridgeExecutionError(`${err.type}: ${err.message}`);
296
- error.traceback = err.traceback;
297
- return error;
298
- }
299
-
300
- private normalizeErrorPayload(
301
- err: unknown
302
- ): { type: string; message: string; traceback?: string } | null {
303
- if (!err || typeof err !== 'object' || Array.isArray(err)) {
304
- return null;
305
- }
306
- const candidate = err as { type?: unknown; message?: unknown; traceback?: unknown };
307
- if (typeof candidate.type !== 'string' || typeof candidate.message !== 'string') {
308
- return null;
309
- }
310
- if (candidate.traceback !== undefined && typeof candidate.traceback !== 'string') {
311
- return null;
312
- }
313
- const normalized: { type: string; message: string; traceback?: string } = {
314
- type: candidate.type,
315
- message: candidate.message,
316
- };
317
- if (typeof candidate.traceback === 'string') {
318
- normalized.traceback = candidate.traceback;
319
- }
320
- return normalized;
321
- }
322
-
323
- private describeInvalidErrorPayload(err: unknown): string {
324
- if (!err || typeof err !== 'object' || Array.isArray(err)) {
325
- return 'expected an object with string "type" and "message" fields';
326
- }
327
- const candidate = err as { type?: unknown; message?: unknown; traceback?: unknown };
328
- if (typeof candidate.type !== 'string') {
329
- return '"type" must be a string';
330
- }
331
- if (typeof candidate.message !== 'string') {
332
- return '"message" must be a string';
333
- }
334
- if (candidate.traceback !== undefined && typeof candidate.traceback !== 'string') {
335
- return '"traceback" must be a string when provided';
336
- }
337
- return 'expected an object with string "type" and "message" fields';
338
- }
339
-
340
- private failRequest(id: number, error: Error): void {
341
- const pending = this.pending.get(id);
342
- if (!pending) {
343
- return;
344
- }
345
- this.pending.delete(id);
346
- if (pending.timer) {
347
- clearTimeout(pending.timer);
348
- }
349
- pending.reject(error);
350
- }
351
-
352
- private rejectAll(error: Error): void {
353
- for (const [, pending] of this.pending) {
354
- if (pending.timer) {
355
- clearTimeout(pending.timer);
356
- }
357
- pending.reject(error);
358
- }
359
- this.pending.clear();
360
- this.timedOutRequests.clear();
361
- }
362
-
363
- private handleProtocolError(details: string, line?: string): void {
364
- if (this.protocolError) {
365
- return;
366
- }
367
- this.protocolError = true;
368
- const snippet = line ? (line.length > 500 ? `${line.slice(0, 500)}...` : line) : undefined;
369
- const hint =
370
- 'Ensure your Python code does not print to stdout and that the bridge outputs only JSON lines.';
371
- const msg = snippet
372
- ? `Protocol error from Python bridge. ${details}\n${hint}\nOffending line: ${snippet}`
373
- : `Protocol error from Python bridge. ${details}\n${hint}`;
374
- const error = new BridgeProtocolError(msg);
375
- this.rejectAll(error);
376
- this.handleFatalError(error);
377
- }
378
-
379
- private handleFatalError(error: BridgeProtocolError): void {
380
- try {
381
- this.options.onFatalError(error);
382
- } catch {
383
- // Ignore callback failures
384
- }
385
- }
386
- }
387
-
388
- export function validateBridgeInfo(info: unknown): void {
389
- if (!info || typeof info !== 'object') {
390
- throw new BridgeProtocolError('Invalid bridge info payload');
391
- }
392
- const candidate = info as BridgeInfo;
393
- if (
394
- candidate.protocol !== TYWRAP_PROTOCOL ||
395
- candidate.protocolVersion !== TYWRAP_PROTOCOL_VERSION
396
- ) {
397
- throw new BridgeProtocolError('Invalid bridge info payload');
398
- }
399
- if (candidate.bridge !== 'python-subprocess') {
400
- throw new BridgeProtocolError(`Unexpected bridge identifier: ${candidate.bridge}`);
401
- }
402
- }
403
-
404
- export function getPathKey(...envs: Array<Record<string, string | undefined>>): string {
405
- for (const env of envs) {
406
- const key = findPathKey(env);
407
- if (key) {
408
- return key;
409
- }
410
- }
411
- return 'PATH';
412
- }
413
-
414
- export function ensurePythonEncoding(env: NodeJS.ProcessEnv): void {
415
- if (env.PYTHONUTF8 === undefined || env.PYTHONUTF8 === null || env.PYTHONUTF8 === '') {
416
- env.PYTHONUTF8 = '1';
417
- }
418
- if (
419
- env.PYTHONIOENCODING === undefined ||
420
- env.PYTHONIOENCODING === null ||
421
- env.PYTHONIOENCODING === ''
422
- ) {
423
- env.PYTHONIOENCODING = 'UTF-8';
424
- }
425
- }
426
-
427
- export function ensureJsonFallback(env: NodeJS.ProcessEnv, enabled: boolean): void {
428
- if (enabled && !env.TYWRAP_CODEC_FALLBACK) {
429
- env.TYWRAP_CODEC_FALLBACK = 'json';
430
- }
431
- }
432
-
433
- export function getMaxLineLengthFromEnv(env: NodeJS.ProcessEnv): number | undefined {
434
- const raw = env.TYWRAP_CODEC_MAX_BYTES?.trim();
435
- if (!raw) {
436
- return undefined;
437
- }
438
- if (!/^\d+$/.test(raw)) {
439
- return undefined;
440
- }
441
- const parsed = Number(raw);
442
- if (!Number.isFinite(parsed) || parsed <= 0) {
443
- return undefined;
444
- }
445
- return parsed;
446
- }
447
-
448
- export function normalizeEnv(
449
- baseEnv: Record<string, string | undefined>,
450
- overrides: Record<string, string | undefined>
451
- ): NodeJS.ProcessEnv {
452
- // Why: normalize PATH casing (Windows) and drop undefined values to avoid spawn surprises.
453
- const overridePathKey = findPathKey(overrides);
454
- const basePathKey = findPathKey(baseEnv);
455
- const pathKey = basePathKey ?? overridePathKey ?? 'PATH';
456
- let pathValue: string | undefined;
457
- if (overridePathKey !== null) {
458
- // eslint-disable-next-line security/detect-object-injection -- env keys are dynamic by design
459
- pathValue = overrides[overridePathKey];
460
- } else if (basePathKey) {
461
- // eslint-disable-next-line security/detect-object-injection -- env keys are dynamic by design
462
- pathValue = baseEnv[basePathKey];
463
- }
464
- const merged: Record<string, string | undefined> = { ...baseEnv, ...overrides };
465
-
466
- const result: NodeJS.ProcessEnv = {};
467
-
468
- if (pathValue !== undefined) {
469
- // eslint-disable-next-line security/detect-object-injection -- env keys are dynamic by design
470
- result[pathKey] = pathValue;
471
- }
472
-
473
- for (const [key, value] of Object.entries(merged)) {
474
- if (value === undefined) {
475
- continue;
476
- }
477
- if (key.toLowerCase() === 'path') {
478
- continue;
479
- }
480
- // eslint-disable-next-line security/detect-object-injection -- env keys are dynamic by design
481
- result[key] = value;
482
- }
483
-
484
- return result;
485
- }
486
-
487
- function findPathKey(env: Record<string, string | undefined>): string | null {
488
- for (const key of Object.keys(env)) {
489
- if (key.toLowerCase() === 'path') {
490
- return key;
491
- }
492
- }
493
- return null;
494
- }