tywrap 0.2.1 → 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.
Files changed (138) hide show
  1. package/README.md +65 -31
  2. package/dist/cli.js +49 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts +11 -7
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/index.js +43 -7
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/core/analyzer.d.ts +1 -0
  9. package/dist/core/analyzer.d.ts.map +1 -1
  10. package/dist/core/analyzer.js +47 -12
  11. package/dist/core/analyzer.js.map +1 -1
  12. package/dist/core/annotation-parser.d.ts +8 -0
  13. package/dist/core/annotation-parser.d.ts.map +1 -0
  14. package/dist/core/annotation-parser.js +409 -0
  15. package/dist/core/annotation-parser.js.map +1 -0
  16. package/dist/core/discovery.d.ts +1 -0
  17. package/dist/core/discovery.d.ts.map +1 -1
  18. package/dist/core/discovery.js +9 -9
  19. package/dist/core/discovery.js.map +1 -1
  20. package/dist/core/generator.d.ts +9 -1
  21. package/dist/core/generator.d.ts.map +1 -1
  22. package/dist/core/generator.js +738 -104
  23. package/dist/core/generator.js.map +1 -1
  24. package/dist/core/mapper.d.ts +9 -7
  25. package/dist/core/mapper.d.ts.map +1 -1
  26. package/dist/core/mapper.js +129 -44
  27. package/dist/core/mapper.js.map +1 -1
  28. package/dist/index.d.ts +7 -7
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +4 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/runtime/bridge-core.d.ts +1 -0
  33. package/dist/runtime/bridge-core.d.ts.map +1 -1
  34. package/dist/runtime/bridge-core.js +52 -17
  35. package/dist/runtime/bridge-core.js.map +1 -1
  36. package/dist/runtime/bridge-protocol.d.ts +17 -0
  37. package/dist/runtime/bridge-protocol.d.ts.map +1 -1
  38. package/dist/runtime/bridge-protocol.js +97 -0
  39. package/dist/runtime/bridge-protocol.js.map +1 -1
  40. package/dist/runtime/errors.d.ts +10 -0
  41. package/dist/runtime/errors.d.ts.map +1 -1
  42. package/dist/runtime/errors.js +9 -0
  43. package/dist/runtime/errors.js.map +1 -1
  44. package/dist/runtime/http-io.d.ts.map +1 -1
  45. package/dist/runtime/http-io.js.map +1 -1
  46. package/dist/runtime/http.d.ts +1 -0
  47. package/dist/runtime/http.d.ts.map +1 -1
  48. package/dist/runtime/http.js +6 -0
  49. package/dist/runtime/http.js.map +1 -1
  50. package/dist/runtime/node.d.ts.map +1 -1
  51. package/dist/runtime/node.js +170 -32
  52. package/dist/runtime/node.js.map +1 -1
  53. package/dist/runtime/optimized-node.d.ts +1 -1
  54. package/dist/runtime/optimized-node.d.ts.map +1 -1
  55. package/dist/runtime/optimized-node.js +1 -1
  56. package/dist/runtime/optimized-node.js.map +1 -1
  57. package/dist/runtime/pooled-transport.d.ts +4 -0
  58. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  59. package/dist/runtime/pooled-transport.js +2 -0
  60. package/dist/runtime/pooled-transport.js.map +1 -1
  61. package/dist/runtime/process-io.d.ts +5 -0
  62. package/dist/runtime/process-io.d.ts.map +1 -1
  63. package/dist/runtime/process-io.js +64 -30
  64. package/dist/runtime/process-io.js.map +1 -1
  65. package/dist/runtime/pyodide-io.d.ts +4 -5
  66. package/dist/runtime/pyodide-io.d.ts.map +1 -1
  67. package/dist/runtime/pyodide-io.js +99 -37
  68. package/dist/runtime/pyodide-io.js.map +1 -1
  69. package/dist/runtime/safe-codec.d.ts +25 -3
  70. package/dist/runtime/safe-codec.d.ts.map +1 -1
  71. package/dist/runtime/safe-codec.js +231 -57
  72. package/dist/runtime/safe-codec.js.map +1 -1
  73. package/dist/runtime/transport.d.ts +6 -1
  74. package/dist/runtime/transport.d.ts.map +1 -1
  75. package/dist/runtime/transport.js.map +1 -1
  76. package/dist/runtime/validators.d.ts +0 -11
  77. package/dist/runtime/validators.d.ts.map +1 -1
  78. package/dist/runtime/validators.js +13 -3
  79. package/dist/runtime/validators.js.map +1 -1
  80. package/dist/runtime/worker-pool.d.ts +12 -0
  81. package/dist/runtime/worker-pool.d.ts.map +1 -1
  82. package/dist/runtime/worker-pool.js +53 -5
  83. package/dist/runtime/worker-pool.js.map +1 -1
  84. package/dist/types/index.d.ts +51 -1
  85. package/dist/types/index.d.ts.map +1 -1
  86. package/dist/tywrap.d.ts.map +1 -1
  87. package/dist/tywrap.js +227 -328
  88. package/dist/tywrap.js.map +1 -1
  89. package/dist/utils/cache.d.ts.map +1 -1
  90. package/dist/utils/cache.js +53 -5
  91. package/dist/utils/cache.js.map +1 -1
  92. package/dist/utils/codec.d.ts.map +1 -1
  93. package/dist/utils/codec.js +10 -16
  94. package/dist/utils/codec.js.map +1 -1
  95. package/dist/utils/ir-cache.d.ts +28 -0
  96. package/dist/utils/ir-cache.d.ts.map +1 -0
  97. package/dist/utils/ir-cache.js +29 -0
  98. package/dist/utils/ir-cache.js.map +1 -0
  99. package/dist/utils/parallel-processor.d.ts +1 -1
  100. package/dist/utils/parallel-processor.d.ts.map +1 -1
  101. package/dist/utils/parallel-processor.js +111 -40
  102. package/dist/utils/parallel-processor.js.map +1 -1
  103. package/dist/utils/runtime.d.ts +5 -1
  104. package/dist/utils/runtime.d.ts.map +1 -1
  105. package/dist/utils/runtime.js +112 -18
  106. package/dist/utils/runtime.js.map +1 -1
  107. package/package.json +40 -18
  108. package/runtime/python_bridge.py +78 -12
  109. package/src/cli.ts +61 -2
  110. package/src/config/index.ts +63 -17
  111. package/src/core/analyzer.ts +52 -15
  112. package/src/core/annotation-parser.ts +500 -0
  113. package/src/core/discovery.ts +26 -12
  114. package/src/core/generator.ts +956 -116
  115. package/src/core/mapper.ts +158 -51
  116. package/src/index.ts +34 -5
  117. package/src/runtime/bridge-core.ts +58 -18
  118. package/src/runtime/bridge-protocol.ts +164 -0
  119. package/src/runtime/errors.ts +14 -0
  120. package/src/runtime/http-io.ts +2 -1
  121. package/src/runtime/http.ts +7 -0
  122. package/src/runtime/node.ts +264 -44
  123. package/src/runtime/optimized-node.ts +4 -1
  124. package/src/runtime/pooled-transport.ts +12 -1
  125. package/src/runtime/process-io.ts +83 -55
  126. package/src/runtime/pyodide-io.ts +110 -44
  127. package/src/runtime/safe-codec.ts +290 -82
  128. package/src/runtime/transport.ts +6 -1
  129. package/src/runtime/validators.ts +14 -3
  130. package/src/runtime/worker-pool.ts +67 -6
  131. package/src/types/global.d.ts +11 -1
  132. package/src/types/index.ts +63 -0
  133. package/src/tywrap.ts +279 -358
  134. package/src/utils/cache.ts +59 -5
  135. package/src/utils/codec.ts +11 -15
  136. package/src/utils/ir-cache.ts +51 -0
  137. package/src/utils/parallel-processor.ts +119 -43
  138. 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 without full parsing.
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
- // Look for "id": <number> (integer IDs)
114
- const match = json.match(/"id"\s*:\s*(-?\d+)/);
115
- return match?.[1] ? parseInt(match[1], 10) : null;
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 (!messageId) {
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 (this.needsRestart || (this.restartAfterRequests > 0 && this.requestCount >= this.restartAfterRequests)) {
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 (!messageId) {
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
- // Response for unknown request - could be for a timed-out request
594
- // Log but don't fail
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(new BridgeTimeoutError(
739
- `Write queue timeout: entry waited ${this.writeQueueTimeoutMs}ms without drain`
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 - queue this write and set draining flag
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
- this.writeQueue.push(this.createQueuedWrite(data, resolve, reject));
808
+ resolve();
786
809
  }
787
810
  } catch (err) {
788
811
  // Synchronous write error (e.g., EPIPE)
789
812
  this.markForRestart();
790
- reject(new BridgeProtocolError(`Write error: ${err instanceof Error ? err.message : 'unknown'}`));
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(new BridgeTimeoutError(
824
- `Write queue timeout: entry waited ${now - queued.queuedAt}ms (limit: ${this.writeQueueTimeoutMs}ms)`
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
- // Still under pressure - put it back with a new timeout
836
- this.writeQueue.unshift(this.createQueuedWrite(
837
- queued.data,
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 error = new BridgeProtocolError(
847
- `Write error: ${err instanceof Error ? err.message : 'unknown'}`
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 { PROTOCOL_ID, type Transport, type ProtocolMessage, type ProtocolResponse } from './transport.js';
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.24.1/full/';
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
- msg_id = msg['id']
88
- msg_type = msg['type']
89
-
90
- if msg_type == 'call':
91
- mod = importlib.import_module(msg['module'])
92
- fn = getattr(mod, msg['functionName'])
93
- result = fn(*msg.get('args', []), **msg.get('kwargs', {}))
94
- return json.dumps({'id': msg_id, 'result': result})
95
-
96
- elif msg_type == 'instantiate':
97
- mod = importlib.import_module(msg['module'])
98
- cls = getattr(mod, msg['className'])
99
- obj = cls(*msg.get('args', []), **msg.get('kwargs', {}))
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
- return json.dumps({'id': msg_id, 'result': handle})
103
-
104
- elif msg_type == 'call_method':
105
- obj = __tywrap_instances[msg['handle']]
106
- method = getattr(obj, msg['methodName'])
107
- result = method(*msg.get('args', []), **msg.get('kwargs', {}))
108
- return json.dumps({'id': msg_id, 'result': result})
109
-
110
- elif msg_type == 'dispose_instance':
111
- __tywrap_instances.pop(msg['handle'], None)
112
- return json.dumps({'id': msg_id, 'result': None})
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
- return json.dumps({
116
- 'id': msg_id,
117
- 'error': {
118
- 'type': 'ValueError',
119
- 'message': f'Unknown message type: {msg_type}'
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': msg.get('id', 'unknown') if msg else 'unknown',
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: '1',
153
- * type: 'call',
154
- * module: 'math',
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 (!parsed.id || !parsed.method) {
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
- `${response.error.type}: ${response.error.message}`,
454
- { code: response.error.type }
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
  }