tywrap 0.2.1 → 0.4.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 +100 -32
- package/dist/cli.js +70 -5
- 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 +62 -20
- 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 +46 -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/dev.d.ts +57 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +743 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- 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 +17 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -1
- package/dist/runtime/bridge-protocol.js +97 -0
- package/dist/runtime/bridge-protocol.js.map +1 -1
- 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.map +1 -1
- package/dist/runtime/http-io.js.map +1 -1
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +6 -0
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +206 -32
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +6 -5
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +6 -5
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +4 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +2 -0
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/process-io.d.ts +5 -0
- package/dist/runtime/process-io.d.ts.map +1 -1
- package/dist/runtime/process-io.js +64 -30
- package/dist/runtime/process-io.js.map +1 -1
- package/dist/runtime/pyodide-io.d.ts +4 -5
- package/dist/runtime/pyodide-io.d.ts.map +1 -1
- package/dist/runtime/pyodide-io.js +99 -37
- package/dist/runtime/pyodide-io.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +25 -3
- package/dist/runtime/safe-codec.d.ts.map +1 -1
- package/dist/runtime/safe-codec.js +231 -57
- package/dist/runtime/safe-codec.js.map +1 -1
- package/dist/runtime/transport.d.ts +6 -1
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +0 -11
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +13 -3
- package/dist/runtime/validators.js.map +1 -1
- package/dist/runtime/worker-pool.d.ts +17 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -1
- package/dist/runtime/worker-pool.js +97 -22
- package/dist/runtime/worker-pool.js.map +1 -1
- package/dist/types/index.d.ts +51 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +6 -0
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +237 -331
- 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 +10 -16
- 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 +47 -21
- package/runtime/python_bridge.py +78 -12
- package/src/cli.ts +85 -5
- package/src/config/index.ts +91 -32
- package/src/core/analyzer.ts +51 -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/dev.ts +983 -0
- package/src/index.ts +35 -6
- package/src/runtime/bridge-core.ts +58 -18
- package/src/runtime/bridge-protocol.ts +164 -0
- package/src/runtime/errors.ts +14 -0
- package/src/runtime/http-io.ts +2 -1
- package/src/runtime/http.ts +7 -0
- package/src/runtime/node.ts +312 -46
- package/src/runtime/optimized-node.ts +9 -5
- package/src/runtime/pooled-transport.ts +12 -1
- package/src/runtime/process-io.ts +83 -55
- package/src/runtime/pyodide-io.ts +110 -44
- package/src/runtime/safe-codec.ts +290 -82
- package/src/runtime/transport.ts +6 -1
- package/src/runtime/validators.ts +14 -3
- package/src/runtime/worker-pool.ts +120 -24
- package/src/types/global.d.ts +11 -1
- package/src/types/index.ts +63 -8
- package/src/tywrap.ts +296 -361
- package/src/utils/cache.ts +59 -5
- package/src/utils/codec.ts +11 -15
- package/src/utils/ir-cache.ts +51 -0
- package/src/utils/parallel-processor.ts +119 -43
- package/src/utils/runtime.ts +129 -18
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
BridgeTimeoutError,
|
|
22
22
|
BridgeExecutionError,
|
|
23
23
|
} from './errors.js';
|
|
24
|
+
import { TimedOutRequestTracker } from './timed-out-request-tracker.js';
|
|
24
25
|
import type { Transport } from './transport.js';
|
|
25
26
|
|
|
26
27
|
// =============================================================================
|
|
@@ -36,6 +37,9 @@ const MAX_STDERR_BYTES = 8 * 1024;
|
|
|
36
37
|
/** Default write queue timeout: 30 seconds */
|
|
37
38
|
const DEFAULT_WRITE_QUEUE_TIMEOUT_MS = 30_000;
|
|
38
39
|
|
|
40
|
+
/** Track timed-out/cancelled request IDs long enough to ignore late responses. */
|
|
41
|
+
const TIMED_OUT_REQUEST_TTL_MS = 10 * 60 * 1000;
|
|
42
|
+
|
|
39
43
|
/** Regex for ANSI escape sequences */
|
|
40
44
|
const ANSI_ESCAPE_RE = /\u001b\[[0-9;]*[A-Za-z]/g;
|
|
41
45
|
|
|
@@ -106,13 +110,27 @@ function sanitizeStderr(value: string): string {
|
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
/**
|
|
109
|
-
* Extract message ID from a JSON string
|
|
113
|
+
* Extract top-level message ID from a JSON string.
|
|
110
114
|
* Returns null if ID cannot be extracted.
|
|
111
115
|
*/
|
|
112
116
|
function extractMessageId(json: string): number | null {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
let parsed: unknown;
|
|
118
|
+
try {
|
|
119
|
+
parsed = JSON.parse(json);
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const id = (parsed as { id?: unknown }).id;
|
|
129
|
+
if (typeof id !== 'number' || !Number.isInteger(id)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return id;
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
// =============================================================================
|
|
@@ -165,6 +183,9 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
165
183
|
|
|
166
184
|
// Request tracking
|
|
167
185
|
private readonly pending = new Map<number, PendingRequest>();
|
|
186
|
+
private readonly timedOutRequests = new TimedOutRequestTracker({
|
|
187
|
+
ttlMs: TIMED_OUT_REQUEST_TTL_MS,
|
|
188
|
+
});
|
|
168
189
|
private requestCount = 0;
|
|
169
190
|
private needsRestart = false;
|
|
170
191
|
|
|
@@ -227,12 +248,15 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
227
248
|
|
|
228
249
|
// Extract message ID for response correlation
|
|
229
250
|
const messageId = extractMessageId(message);
|
|
230
|
-
if (
|
|
251
|
+
if (messageId === null) {
|
|
231
252
|
throw new BridgeProtocolError('Message must contain an "id" field');
|
|
232
253
|
}
|
|
233
254
|
|
|
234
255
|
// Check for restart condition (either scheduled restart or forced by stream error)
|
|
235
|
-
if (
|
|
256
|
+
if (
|
|
257
|
+
this.needsRestart ||
|
|
258
|
+
(this.restartAfterRequests > 0 && this.requestCount >= this.restartAfterRequests)
|
|
259
|
+
) {
|
|
236
260
|
await this.restartProcess();
|
|
237
261
|
}
|
|
238
262
|
|
|
@@ -243,6 +267,7 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
243
267
|
if (timeoutMs > 0) {
|
|
244
268
|
timer = setTimeout(() => {
|
|
245
269
|
this.pending.delete(messageId);
|
|
270
|
+
this.timedOutRequests.mark(messageId);
|
|
246
271
|
const stderrTail = this.getStderrTail();
|
|
247
272
|
const baseMsg = `Operation timed out after ${timeoutMs}ms`;
|
|
248
273
|
const msg = stderrTail ? `${baseMsg}. Recent stderr:\n${stderrTail}` : baseMsg;
|
|
@@ -256,6 +281,7 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
256
281
|
clearTimeout(timer);
|
|
257
282
|
}
|
|
258
283
|
this.pending.delete(messageId);
|
|
284
|
+
this.timedOutRequests.mark(messageId);
|
|
259
285
|
reject(new BridgeTimeoutError('Operation aborted'));
|
|
260
286
|
};
|
|
261
287
|
|
|
@@ -318,9 +344,7 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
318
344
|
protected async doDispose(): Promise<void> {
|
|
319
345
|
// Reject all pending requests
|
|
320
346
|
const stderrTail = this.getStderrTail();
|
|
321
|
-
const msg = stderrTail
|
|
322
|
-
? `Transport disposed. Stderr:\n${stderrTail}`
|
|
323
|
-
: 'Transport disposed';
|
|
347
|
+
const msg = stderrTail ? `Transport disposed. Stderr:\n${stderrTail}` : 'Transport disposed';
|
|
324
348
|
const error = new BridgeDisposedError(msg);
|
|
325
349
|
|
|
326
350
|
for (const [, pending] of this.pending) {
|
|
@@ -344,6 +368,7 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
344
368
|
// Clear buffers
|
|
345
369
|
this.stdoutBuffer = '';
|
|
346
370
|
this.stderrBuffer = '';
|
|
371
|
+
this.timedOutRequests.clear();
|
|
347
372
|
this.requestCount = 0;
|
|
348
373
|
}
|
|
349
374
|
|
|
@@ -539,16 +564,10 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
539
564
|
this.stdoutBuffer += chunk.toString();
|
|
540
565
|
|
|
541
566
|
// Check for excessive line length without newline
|
|
542
|
-
if (
|
|
543
|
-
this.stdoutBuffer.length > this.maxLineLength &&
|
|
544
|
-
!this.stdoutBuffer.includes('\n')
|
|
545
|
-
) {
|
|
567
|
+
if (this.stdoutBuffer.length > this.maxLineLength && !this.stdoutBuffer.includes('\n')) {
|
|
546
568
|
const snippet = this.stdoutBuffer.slice(0, 500);
|
|
547
569
|
this.stdoutBuffer = '';
|
|
548
|
-
this.handleProtocolError(
|
|
549
|
-
`Response line exceeded ${this.maxLineLength} bytes`,
|
|
550
|
-
snippet
|
|
551
|
-
);
|
|
570
|
+
this.handleProtocolError(`Response line exceeded ${this.maxLineLength} bytes`, snippet);
|
|
552
571
|
return;
|
|
553
572
|
}
|
|
554
573
|
|
|
@@ -566,10 +585,7 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
566
585
|
// Check line length
|
|
567
586
|
if (line.length > this.maxLineLength) {
|
|
568
587
|
const snippet = line.slice(0, 500);
|
|
569
|
-
this.handleProtocolError(
|
|
570
|
-
`Response line exceeded ${this.maxLineLength} bytes`,
|
|
571
|
-
snippet
|
|
572
|
-
);
|
|
588
|
+
this.handleProtocolError(`Response line exceeded ${this.maxLineLength} bytes`, snippet);
|
|
573
589
|
return;
|
|
574
590
|
}
|
|
575
591
|
|
|
@@ -583,15 +599,19 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
583
599
|
private handleResponseLine(line: string): void {
|
|
584
600
|
// Extract ID to find pending request
|
|
585
601
|
const messageId = extractMessageId(line);
|
|
586
|
-
if (
|
|
602
|
+
if (messageId === null) {
|
|
587
603
|
this.handleProtocolError('Response missing "id" field', line);
|
|
588
604
|
return;
|
|
589
605
|
}
|
|
590
606
|
|
|
591
607
|
const pending = this.pending.get(messageId);
|
|
592
608
|
if (!pending) {
|
|
593
|
-
//
|
|
594
|
-
|
|
609
|
+
// Ignore expected late responses from timed-out/cancelled requests.
|
|
610
|
+
if (this.timedOutRequests.consume(messageId)) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
// Unknown IDs while requests are pending indicate protocol desync.
|
|
614
|
+
this.handleProtocolError(`Unexpected response id ${messageId}`, line);
|
|
595
615
|
return;
|
|
596
616
|
}
|
|
597
617
|
|
|
@@ -616,9 +636,7 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
616
636
|
|
|
617
637
|
// Keep only the tail
|
|
618
638
|
if (this.stderrBuffer.length > MAX_STDERR_BYTES) {
|
|
619
|
-
this.stderrBuffer = this.stderrBuffer.slice(
|
|
620
|
-
this.stderrBuffer.length - MAX_STDERR_BYTES
|
|
621
|
-
);
|
|
639
|
+
this.stderrBuffer = this.stderrBuffer.slice(this.stderrBuffer.length - MAX_STDERR_BYTES);
|
|
622
640
|
}
|
|
623
641
|
} catch {
|
|
624
642
|
// Ignore stderr buffering errors
|
|
@@ -676,7 +694,7 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
676
694
|
*/
|
|
677
695
|
private handleStdinError(err: Error): void {
|
|
678
696
|
// EPIPE means process died
|
|
679
|
-
const error = new BridgeProtocolError(`stdin error: ${err.message}`);
|
|
697
|
+
const error = new BridgeProtocolError(this.withStderrTail(`stdin error: ${err.message}`));
|
|
680
698
|
|
|
681
699
|
// Reject all pending writes
|
|
682
700
|
for (const queued of this.writeQueue) {
|
|
@@ -697,7 +715,7 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
697
715
|
* This can occur during pipe errors or when the process crashes.
|
|
698
716
|
*/
|
|
699
717
|
private handleStdoutError(err: Error): void {
|
|
700
|
-
const error = new BridgeProtocolError(`stdout error: ${err.message}`);
|
|
718
|
+
const error = new BridgeProtocolError(this.withStderrTail(`stdout error: ${err.message}`));
|
|
701
719
|
this.rejectAllPending(error);
|
|
702
720
|
this.markForRestart();
|
|
703
721
|
}
|
|
@@ -708,7 +726,7 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
708
726
|
*/
|
|
709
727
|
private handleStderrError(err: Error): void {
|
|
710
728
|
// Stderr errors are less critical but still indicate process health issues
|
|
711
|
-
const error = new BridgeProtocolError(`stderr error: ${err.message}`);
|
|
729
|
+
const error = new BridgeProtocolError(this.withStderrTail(`stderr error: ${err.message}`));
|
|
712
730
|
this.rejectAllPending(error);
|
|
713
731
|
this.markForRestart();
|
|
714
732
|
}
|
|
@@ -735,14 +753,18 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
735
753
|
const index = this.writeQueue.indexOf(entry);
|
|
736
754
|
if (index !== -1) {
|
|
737
755
|
this.writeQueue.splice(index, 1);
|
|
738
|
-
reject(
|
|
739
|
-
|
|
740
|
-
|
|
756
|
+
reject(
|
|
757
|
+
new BridgeTimeoutError(
|
|
758
|
+
`Write queue timeout: entry waited ${this.writeQueueTimeoutMs}ms without drain`
|
|
759
|
+
)
|
|
760
|
+
);
|
|
741
761
|
}
|
|
742
762
|
}, this.writeQueueTimeoutMs);
|
|
743
763
|
|
|
744
|
-
// Unref the timer so it doesn't keep the process alive
|
|
745
|
-
entry.timeoutHandle.unref
|
|
764
|
+
// Unref the timer so it doesn't keep the process alive (best-effort for non-Node runtimes)
|
|
765
|
+
if (typeof entry.timeoutHandle.unref === 'function') {
|
|
766
|
+
entry.timeoutHandle.unref();
|
|
767
|
+
}
|
|
746
768
|
|
|
747
769
|
return entry;
|
|
748
770
|
}
|
|
@@ -763,7 +785,7 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
763
785
|
private writeToStdin(data: string): Promise<void> {
|
|
764
786
|
return new Promise<void>((resolve, reject) => {
|
|
765
787
|
if (!this.process?.stdin || this.processExited) {
|
|
766
|
-
reject(new BridgeProtocolError('Process stdin not available'));
|
|
788
|
+
reject(new BridgeProtocolError(this.withStderrTail('Process stdin not available')));
|
|
767
789
|
return;
|
|
768
790
|
}
|
|
769
791
|
|
|
@@ -780,14 +802,16 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
780
802
|
if (canWrite) {
|
|
781
803
|
resolve();
|
|
782
804
|
} else {
|
|
783
|
-
// Backpressure -
|
|
805
|
+
// Backpressure - current write is accepted by Node's stream buffer.
|
|
806
|
+
// We only pause subsequent writes until "drain".
|
|
784
807
|
this.draining = true;
|
|
785
|
-
|
|
808
|
+
resolve();
|
|
786
809
|
}
|
|
787
810
|
} catch (err) {
|
|
788
811
|
// Synchronous write error (e.g., EPIPE)
|
|
789
812
|
this.markForRestart();
|
|
790
|
-
|
|
813
|
+
const errorMessage = err instanceof Error ? err.message : 'unknown';
|
|
814
|
+
reject(new BridgeProtocolError(this.withStderrTail(`Write error: ${errorMessage}`)));
|
|
791
815
|
}
|
|
792
816
|
});
|
|
793
817
|
}
|
|
@@ -803,7 +827,7 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
803
827
|
// Process died - reject all queued writes
|
|
804
828
|
for (const q of this.writeQueue) {
|
|
805
829
|
this.clearQueuedWriteTimeout(q);
|
|
806
|
-
q.reject(new BridgeProtocolError('Process stdin not available'));
|
|
830
|
+
q.reject(new BridgeProtocolError(this.withStderrTail('Process stdin not available')));
|
|
807
831
|
}
|
|
808
832
|
this.writeQueue.length = 0;
|
|
809
833
|
this.markForRestart();
|
|
@@ -820,9 +844,11 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
820
844
|
|
|
821
845
|
// Check for write queue timeout (fallback check, timer should have handled this)
|
|
822
846
|
if (now - queued.queuedAt > this.writeQueueTimeoutMs) {
|
|
823
|
-
queued.reject(
|
|
824
|
-
|
|
825
|
-
|
|
847
|
+
queued.reject(
|
|
848
|
+
new BridgeTimeoutError(
|
|
849
|
+
`Write queue timeout: entry waited ${now - queued.queuedAt}ms (limit: ${this.writeQueueTimeoutMs}ms)`
|
|
850
|
+
)
|
|
851
|
+
);
|
|
826
852
|
continue; // Process next entry
|
|
827
853
|
}
|
|
828
854
|
|
|
@@ -832,20 +858,16 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
832
858
|
if (canWrite) {
|
|
833
859
|
queued.resolve();
|
|
834
860
|
} else {
|
|
835
|
-
//
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
queued.resolve,
|
|
839
|
-
queued.reject
|
|
840
|
-
));
|
|
861
|
+
// Backpressure - this write has been accepted by stream buffer.
|
|
862
|
+
// Pause further queued writes until the next "drain" event.
|
|
863
|
+
queued.resolve();
|
|
841
864
|
this.draining = true;
|
|
842
865
|
return;
|
|
843
866
|
}
|
|
844
867
|
} catch (err) {
|
|
845
868
|
// Synchronous write error (e.g., EPIPE) - reject this and all remaining writes
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
);
|
|
869
|
+
const errorMessage = err instanceof Error ? err.message : 'unknown';
|
|
870
|
+
const error = new BridgeProtocolError(this.withStderrTail(`Write error: ${errorMessage}`));
|
|
849
871
|
queued.reject(error);
|
|
850
872
|
for (const q of this.writeQueue) {
|
|
851
873
|
this.clearQueuedWriteTimeout(q);
|
|
@@ -869,13 +891,19 @@ export class ProcessIO extends BoundedContext implements Transport {
|
|
|
869
891
|
return this.stderrBuffer.trim();
|
|
870
892
|
}
|
|
871
893
|
|
|
894
|
+
/**
|
|
895
|
+
* Append stderr context when available.
|
|
896
|
+
*/
|
|
897
|
+
private withStderrTail(message: string): string {
|
|
898
|
+
const stderrTail = this.getStderrTail();
|
|
899
|
+
return stderrTail ? `${message}. Stderr:\n${stderrTail}` : message;
|
|
900
|
+
}
|
|
901
|
+
|
|
872
902
|
/**
|
|
873
903
|
* Handle a protocol error by rejecting all pending requests.
|
|
874
904
|
*/
|
|
875
905
|
private handleProtocolError(details: string, line?: string): void {
|
|
876
|
-
const snippet = line
|
|
877
|
-
? line.length > 500 ? `${line.slice(0, 500)}...` : line
|
|
878
|
-
: undefined;
|
|
906
|
+
const snippet = line ? (line.length > 500 ? `${line.slice(0, 500)}...` : line) : undefined;
|
|
879
907
|
|
|
880
908
|
const hint = 'Ensure Python code does not print to stdout and bridge outputs only JSON lines.';
|
|
881
909
|
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
import { BoundedContext } from './bounded-context.js';
|
|
18
18
|
import { BridgeExecutionError, BridgeProtocolError } from './errors.js';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
PROTOCOL_ID,
|
|
21
|
+
type Transport,
|
|
22
|
+
type ProtocolMessage,
|
|
23
|
+
type ProtocolResponse,
|
|
24
|
+
} from './transport.js';
|
|
20
25
|
|
|
21
26
|
// =============================================================================
|
|
22
27
|
// TYPES
|
|
@@ -55,7 +60,7 @@ interface PyodideInstance {
|
|
|
55
60
|
// =============================================================================
|
|
56
61
|
|
|
57
62
|
/** Default Pyodide CDN URL */
|
|
58
|
-
const DEFAULT_INDEX_URL = 'https://cdn.jsdelivr.net/pyodide/v0.
|
|
63
|
+
const DEFAULT_INDEX_URL = 'https://cdn.jsdelivr.net/pyodide/v0.28.0/full/';
|
|
59
64
|
|
|
60
65
|
/**
|
|
61
66
|
* Bootstrap Python code that sets up the dispatch function.
|
|
@@ -70,6 +75,27 @@ import json
|
|
|
70
75
|
import importlib
|
|
71
76
|
|
|
72
77
|
__tywrap_instances = {}
|
|
78
|
+
__tywrap_protocol = 'tywrap/1'
|
|
79
|
+
|
|
80
|
+
def __tywrap_require_str(params, key):
|
|
81
|
+
value = params.get(key)
|
|
82
|
+
if not isinstance(value, str) or not value:
|
|
83
|
+
raise ValueError(f'Missing {key}')
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
def __tywrap_coerce_list(value, key):
|
|
87
|
+
if value is None:
|
|
88
|
+
return []
|
|
89
|
+
if not isinstance(value, list):
|
|
90
|
+
raise ValueError(f'Invalid {key}')
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
def __tywrap_coerce_dict(value, key):
|
|
94
|
+
if value is None:
|
|
95
|
+
return {}
|
|
96
|
+
if not isinstance(value, dict):
|
|
97
|
+
raise ValueError(f'Invalid {key}')
|
|
98
|
+
return value
|
|
73
99
|
|
|
74
100
|
def __tywrap_dispatch(message_json):
|
|
75
101
|
"""
|
|
@@ -82,48 +108,76 @@ def __tywrap_dispatch(message_json):
|
|
|
82
108
|
JSON-encoded ProtocolResponse string
|
|
83
109
|
"""
|
|
84
110
|
msg = None
|
|
111
|
+
msg_id = -1
|
|
85
112
|
try:
|
|
86
113
|
msg = json.loads(message_json)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
114
|
+
if not isinstance(msg, dict):
|
|
115
|
+
raise ValueError('Invalid request payload')
|
|
116
|
+
|
|
117
|
+
msg_id = msg.get('id')
|
|
118
|
+
if not isinstance(msg_id, int):
|
|
119
|
+
raise ValueError(f'Invalid request id: {msg_id}')
|
|
120
|
+
|
|
121
|
+
protocol = msg.get('protocol')
|
|
122
|
+
if protocol != __tywrap_protocol:
|
|
123
|
+
raise ValueError(f'Invalid protocol: {protocol}')
|
|
124
|
+
|
|
125
|
+
method = msg.get('method')
|
|
126
|
+
if not isinstance(method, str):
|
|
127
|
+
raise ValueError('Missing method')
|
|
128
|
+
|
|
129
|
+
params = __tywrap_coerce_dict(msg.get('params'), 'params')
|
|
130
|
+
|
|
131
|
+
if method == 'call':
|
|
132
|
+
module_name = __tywrap_require_str(params, 'module')
|
|
133
|
+
function_name = __tywrap_require_str(params, 'functionName')
|
|
134
|
+
args = __tywrap_coerce_list(params.get('args'), 'args')
|
|
135
|
+
kwargs = __tywrap_coerce_dict(params.get('kwargs'), 'kwargs')
|
|
136
|
+
mod = importlib.import_module(module_name)
|
|
137
|
+
fn = getattr(mod, function_name)
|
|
138
|
+
result = fn(*args, **kwargs)
|
|
139
|
+
|
|
140
|
+
elif method == 'instantiate':
|
|
141
|
+
module_name = __tywrap_require_str(params, 'module')
|
|
142
|
+
class_name = __tywrap_require_str(params, 'className')
|
|
143
|
+
args = __tywrap_coerce_list(params.get('args'), 'args')
|
|
144
|
+
kwargs = __tywrap_coerce_dict(params.get('kwargs'), 'kwargs')
|
|
145
|
+
mod = importlib.import_module(module_name)
|
|
146
|
+
cls = getattr(mod, class_name)
|
|
147
|
+
obj = cls(*args, **kwargs)
|
|
100
148
|
handle = str(id(obj))
|
|
101
149
|
__tywrap_instances[handle] = obj
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
elif
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
__tywrap_instances
|
|
112
|
-
|
|
150
|
+
result = handle
|
|
151
|
+
|
|
152
|
+
elif method == 'call_method':
|
|
153
|
+
handle = __tywrap_require_str(params, 'handle')
|
|
154
|
+
method_name = __tywrap_require_str(params, 'methodName')
|
|
155
|
+
args = __tywrap_coerce_list(params.get('args'), 'args')
|
|
156
|
+
kwargs = __tywrap_coerce_dict(params.get('kwargs'), 'kwargs')
|
|
157
|
+
if handle not in __tywrap_instances:
|
|
158
|
+
raise ValueError(f'Unknown instance handle: {handle}')
|
|
159
|
+
obj = __tywrap_instances[handle]
|
|
160
|
+
bound_method = getattr(obj, method_name)
|
|
161
|
+
result = bound_method(*args, **kwargs)
|
|
162
|
+
|
|
163
|
+
elif method == 'dispose_instance':
|
|
164
|
+
handle = __tywrap_require_str(params, 'handle')
|
|
165
|
+
result = __tywrap_instances.pop(handle, None) is not None
|
|
113
166
|
|
|
114
167
|
else:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
168
|
+
raise ValueError(f'Unknown method: {method}')
|
|
169
|
+
|
|
170
|
+
return json.dumps({
|
|
171
|
+
'id': msg_id,
|
|
172
|
+
'protocol': __tywrap_protocol,
|
|
173
|
+
'result': result
|
|
174
|
+
})
|
|
122
175
|
|
|
123
176
|
except Exception as e:
|
|
124
177
|
import traceback
|
|
125
178
|
return json.dumps({
|
|
126
|
-
'id':
|
|
179
|
+
'id': msg_id,
|
|
180
|
+
'protocol': __tywrap_protocol,
|
|
127
181
|
'error': {
|
|
128
182
|
'type': type(e).__name__,
|
|
129
183
|
'message': str(e),
|
|
@@ -149,11 +203,10 @@ def __tywrap_dispatch(message_json):
|
|
|
149
203
|
*
|
|
150
204
|
* const response = await transport.send(
|
|
151
205
|
* JSON.stringify({
|
|
152
|
-
* id:
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* functionName: 'sqrt',
|
|
156
|
-
* args: [16]
|
|
206
|
+
* id: 1,
|
|
207
|
+
* protocol: 'tywrap/1',
|
|
208
|
+
* method: 'call',
|
|
209
|
+
* params: { module: 'math', functionName: 'sqrt', args: [16], kwargs: {} }
|
|
157
210
|
* }),
|
|
158
211
|
* 5000
|
|
159
212
|
* );
|
|
@@ -262,7 +315,13 @@ export class PyodideIO extends BoundedContext implements Transport {
|
|
|
262
315
|
}
|
|
263
316
|
|
|
264
317
|
// Validate required fields
|
|
265
|
-
if (
|
|
318
|
+
if (typeof parsed.id !== 'number' || !Number.isInteger(parsed.id)) {
|
|
319
|
+
throw new BridgeProtocolError('Message missing required fields: id, method');
|
|
320
|
+
}
|
|
321
|
+
if (parsed.protocol !== PROTOCOL_ID || typeof parsed.method !== 'string') {
|
|
322
|
+
throw new BridgeProtocolError('Message missing required fields: id, method');
|
|
323
|
+
}
|
|
324
|
+
if (!parsed.params || typeof parsed.params !== 'object' || Array.isArray(parsed.params)) {
|
|
266
325
|
throw new BridgeProtocolError('Message missing required fields: id, method');
|
|
267
326
|
}
|
|
268
327
|
|
|
@@ -280,6 +339,14 @@ export class PyodideIO extends BoundedContext implements Transport {
|
|
|
280
339
|
// Validate response is valid JSON
|
|
281
340
|
try {
|
|
282
341
|
const response = JSON.parse(responseJson) as ProtocolResponse;
|
|
342
|
+
if (typeof response.id !== 'number' || !Number.isInteger(response.id)) {
|
|
343
|
+
throw new BridgeProtocolError('Invalid response from Python: missing numeric id');
|
|
344
|
+
}
|
|
345
|
+
if (response.protocol !== undefined && response.protocol !== PROTOCOL_ID) {
|
|
346
|
+
throw new BridgeProtocolError(
|
|
347
|
+
`Invalid protocol version: expected "${PROTOCOL_ID}", got "${response.protocol}"`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
283
350
|
if (response.error) {
|
|
284
351
|
// Return the response as-is; let the caller handle the error
|
|
285
352
|
return responseJson;
|
|
@@ -449,10 +516,9 @@ export class PyodideIO extends BoundedContext implements Transport {
|
|
|
449
516
|
const response = JSON.parse(responseJson) as ProtocolResponse;
|
|
450
517
|
|
|
451
518
|
if (response.error) {
|
|
452
|
-
const err = new BridgeExecutionError(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
);
|
|
519
|
+
const err = new BridgeExecutionError(`${response.error.type}: ${response.error.message}`, {
|
|
520
|
+
code: response.error.type,
|
|
521
|
+
});
|
|
456
522
|
err.traceback = response.error.traceback;
|
|
457
523
|
throw err;
|
|
458
524
|
}
|