tywrap 0.6.1 → 0.8.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 (110) hide show
  1. package/README.md +15 -5
  2. package/dist/core/annotation-parser.d.ts.map +1 -1
  3. package/dist/core/annotation-parser.js.map +1 -1
  4. package/dist/core/emit-call.d.ts.map +1 -1
  5. package/dist/core/emit-call.js +1 -1
  6. package/dist/core/emit-call.js.map +1 -1
  7. package/dist/core/generator.d.ts.map +1 -1
  8. package/dist/core/generator.js +40 -9
  9. package/dist/core/generator.js.map +1 -1
  10. package/dist/dev.d.ts.map +1 -1
  11. package/dist/dev.js +1 -3
  12. package/dist/dev.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/runtime/base-bridge.d.ts +57 -0
  17. package/dist/runtime/base-bridge.d.ts.map +1 -0
  18. package/dist/runtime/base-bridge.js +72 -0
  19. package/dist/runtime/base-bridge.js.map +1 -0
  20. package/dist/runtime/frame-codec.d.ts +111 -0
  21. package/dist/runtime/frame-codec.d.ts.map +1 -0
  22. package/dist/runtime/frame-codec.js +352 -0
  23. package/dist/runtime/frame-codec.js.map +1 -0
  24. package/dist/runtime/http-transport.d.ts +11 -1
  25. package/dist/runtime/http-transport.d.ts.map +1 -1
  26. package/dist/runtime/http-transport.js +19 -0
  27. package/dist/runtime/http-transport.js.map +1 -1
  28. package/dist/runtime/http.d.ts +5 -12
  29. package/dist/runtime/http.d.ts.map +1 -1
  30. package/dist/runtime/http.js +6 -29
  31. package/dist/runtime/http.js.map +1 -1
  32. package/dist/runtime/index.d.ts +2 -2
  33. package/dist/runtime/index.d.ts.map +1 -1
  34. package/dist/runtime/index.js +1 -1
  35. package/dist/runtime/index.js.map +1 -1
  36. package/dist/runtime/node.d.ts +25 -19
  37. package/dist/runtime/node.d.ts.map +1 -1
  38. package/dist/runtime/node.js +19 -34
  39. package/dist/runtime/node.js.map +1 -1
  40. package/dist/runtime/pooled-transport.d.ts +21 -2
  41. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  42. package/dist/runtime/pooled-transport.js +16 -0
  43. package/dist/runtime/pooled-transport.js.map +1 -1
  44. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
  45. package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
  46. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
  47. package/dist/runtime/pyodide-transport.d.ts +12 -1
  48. package/dist/runtime/pyodide-transport.d.ts.map +1 -1
  49. package/dist/runtime/pyodide-transport.js +20 -0
  50. package/dist/runtime/pyodide-transport.js.map +1 -1
  51. package/dist/runtime/pyodide.d.ts +5 -12
  52. package/dist/runtime/pyodide.d.ts.map +1 -1
  53. package/dist/runtime/pyodide.js +6 -29
  54. package/dist/runtime/pyodide.js.map +1 -1
  55. package/dist/runtime/rpc-client.d.ts +14 -1
  56. package/dist/runtime/rpc-client.d.ts.map +1 -1
  57. package/dist/runtime/rpc-client.js +68 -6
  58. package/dist/runtime/rpc-client.js.map +1 -1
  59. package/dist/runtime/subprocess-transport.d.ts +177 -3
  60. package/dist/runtime/subprocess-transport.d.ts.map +1 -1
  61. package/dist/runtime/subprocess-transport.js +526 -26
  62. package/dist/runtime/subprocess-transport.js.map +1 -1
  63. package/dist/runtime/transport.d.ts +141 -0
  64. package/dist/runtime/transport.d.ts.map +1 -1
  65. package/dist/runtime/transport.js +21 -0
  66. package/dist/runtime/transport.js.map +1 -1
  67. package/dist/types/index.d.ts +59 -0
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/dist/tywrap.d.ts.map +1 -1
  70. package/dist/tywrap.js +204 -149
  71. package/dist/tywrap.js.map +1 -1
  72. package/dist/utils/codec.d.ts +2 -0
  73. package/dist/utils/codec.d.ts.map +1 -1
  74. package/dist/utils/codec.js +205 -6
  75. package/dist/utils/codec.js.map +1 -1
  76. package/dist/version.js +1 -1
  77. package/package.json +7 -1
  78. package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
  79. package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
  80. package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
  81. package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
  82. package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
  83. package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
  84. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  85. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  86. package/runtime/frame_codec.py +424 -0
  87. package/runtime/python_bridge.py +241 -42
  88. package/runtime/tywrap_bridge_core.py +152 -13
  89. package/src/core/annotation-parser.ts +2 -1
  90. package/src/core/emit-call.ts +1 -7
  91. package/src/core/generator.ts +50 -11
  92. package/src/dev.ts +1 -3
  93. package/src/index.ts +1 -0
  94. package/src/runtime/base-bridge.ts +106 -0
  95. package/src/runtime/frame-codec.ts +469 -0
  96. package/src/runtime/http-transport.ts +21 -1
  97. package/src/runtime/http.ts +7 -51
  98. package/src/runtime/index.ts +2 -6
  99. package/src/runtime/node.ts +42 -53
  100. package/src/runtime/pooled-transport.ts +25 -2
  101. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  102. package/src/runtime/pyodide-transport.ts +22 -0
  103. package/src/runtime/pyodide.ts +7 -52
  104. package/src/runtime/rpc-client.ts +91 -7
  105. package/src/runtime/subprocess-transport.ts +629 -30
  106. package/src/runtime/transport.ts +169 -0
  107. package/src/types/index.ts +62 -0
  108. package/src/tywrap.ts +265 -162
  109. package/src/utils/codec.ts +245 -7
  110. package/src/version.ts +1 -1
@@ -26,6 +26,28 @@ export const PROTOCOL_ID = 'tywrap/1';
26
26
  */
27
27
  export const TYWRAP_PROTOCOL_VERSION = Number.parseInt(PROTOCOL_ID.split('/')[1] ?? '', 10);
28
28
 
29
+ /**
30
+ * Framing protocol identifier for chunked large-payload transport.
31
+ *
32
+ * This is DISTINCT from {@link PROTOCOL_ID}: the logical RPC stays `tywrap/1`,
33
+ * while `tywrap-frame/1` describes a separate layer (below {@link Transport.send})
34
+ * that fragments one logical message across multiple wire frames and reassembles
35
+ * it. An old bridge rejects any non-`tywrap/1` request, so the logical protocol
36
+ * must NOT be bumped to negotiate chunking — a separate framing protocol,
37
+ * advertised through a `tywrap/1` `meta` extension, is used instead.
38
+ *
39
+ * Subprocess-only for 0.8.0 (it is the only backend with a real frame ceiling —
40
+ * the JSONL line-length limit). See docs/transport-framing.md.
41
+ */
42
+ export const FRAME_PROTOCOL_ID = 'tywrap-frame/1';
43
+
44
+ /**
45
+ * Numeric framing-protocol version. Derived from the trailing number of
46
+ * {@link FRAME_PROTOCOL_ID} so the two cannot drift — bump FRAME_PROTOCOL_ID
47
+ * alone and this follows (same pattern as {@link TYWRAP_PROTOCOL_VERSION}).
48
+ */
49
+ export const FRAME_PROTOCOL_VERSION = Number.parseInt(FRAME_PROTOCOL_ID.split('/')[1] ?? '', 10);
50
+
29
51
  // =============================================================================
30
52
  // PROTOCOL TYPES
31
53
  // =============================================================================
@@ -102,6 +124,142 @@ export interface ProtocolResponse {
102
124
  };
103
125
  }
104
126
 
127
+ // =============================================================================
128
+ // CHUNK FRAMING (tywrap-frame/1)
129
+ // =============================================================================
130
+
131
+ /**
132
+ * Per-frame encoding for a {@link ChunkFrame}'s `data` field.
133
+ *
134
+ * - `utf8-slice` (the chosen default for 0.8.0): `data` is a raw substring of the
135
+ * complete logical JSON message, split on UTF-8 codepoint boundaries. Because
136
+ * the logical payload is already valid-UTF-8 JSON, the slices reassemble by
137
+ * simple concatenation — no inflation, no extra decode. See
138
+ * docs/transport-framing.md for the rationale (decision #6).
139
+ * - `utf8-base64`: `data` is a base64-encoded chunk of the UTF-8 bytes, safe for
140
+ * arbitrary byte splits but ~33% larger on the wire with a memory-amplification
141
+ * cost. Reserved as an alternative; not emitted by tywrap in 0.8.0.
142
+ */
143
+ export type ChunkFrameEncoding = 'utf8-base64' | 'utf8-slice';
144
+
145
+ /**
146
+ * A single wire frame of the `tywrap-frame/1` framing protocol.
147
+ *
148
+ * A frame envelope is DISTINCT from the logical {@link ProtocolMessage} /
149
+ * {@link ProtocolResponse}: it carries a slice of the bytes of ONE complete
150
+ * logical JSON message (a request or a response), fragmented because the payload
151
+ * exceeds the transport's frame ceiling. The framing layer reassembles all
152
+ * frames for a given {@link ChunkFrame.id} back into the single logical message
153
+ * before the JSON/codec path ever sees it.
154
+ *
155
+ * Correlation reuses the existing RPC `id`. `seq` is zero-based; `total` and
156
+ * `totalBytes` are repeated on every frame so the receiver can validate the
157
+ * stream is complete (no missing/duplicate `seq`, exact frame count, exact
158
+ * reassembled byte length) before decoding.
159
+ *
160
+ * @see docs/transport-framing.md
161
+ */
162
+ export interface ChunkFrame {
163
+ /**
164
+ * Frame-envelope discriminator.
165
+ * - `'chunk'`: a normal data-carrying frame.
166
+ * - `'error'`: a framing-layer error (e.g. the sender could not continue the
167
+ * stream); carries no further data frames for this `id`.
168
+ */
169
+ __tywrap_frame__: 'chunk' | 'error';
170
+
171
+ /** Framing protocol identifier (must equal {@link FRAME_PROTOCOL_ID}). */
172
+ frameProtocol: string;
173
+
174
+ /** Which logical stream this frame belongs to. */
175
+ stream: 'request' | 'response';
176
+
177
+ /** RPC correlation id, shared with the logical {@link ProtocolMessage.id}. */
178
+ id: number;
179
+
180
+ /** Zero-based sequence index of this frame within its stream. */
181
+ seq: number;
182
+
183
+ /** Total number of frames in this stream (repeated on every frame). */
184
+ total: number;
185
+
186
+ /**
187
+ * Total byte length of the complete reassembled logical message (repeated on
188
+ * every frame). Used to validate the reassembled payload exactly.
189
+ */
190
+ totalBytes: number;
191
+
192
+ /** Per-frame payload encoding (see {@link ChunkFrameEncoding}). */
193
+ encoding: ChunkFrameEncoding;
194
+
195
+ /** This frame's slice of the logical message, encoded per {@link encoding}. */
196
+ data: string;
197
+ }
198
+
199
+ // =============================================================================
200
+ // TRANSPORT CAPABILITIES
201
+ // =============================================================================
202
+
203
+ /**
204
+ * Static, transport-level capability descriptor.
205
+ *
206
+ * Each backend exposes one of these via {@link Transport.capabilities} so callers
207
+ * can reason about what the wire channel can carry WITHOUT round-tripping to
208
+ * Python. These flags describe the transport itself (what bytes it can move and
209
+ * how it frames them) — they are deliberately separate from the bridge's runtime
210
+ * `meta` report ({@link BridgeInfo}), which describes the *Python environment*
211
+ * (which optional libraries happen to be importable). The transport descriptor is
212
+ * authoritative for transport-level flags; the meta report is authoritative for
213
+ * library availability.
214
+ *
215
+ * Honest for TODAY's behavior: as of 0.8.0 `supportsChunking` is implemented for
216
+ * the subprocess backend and reports the *configured* `tywrap-frame/1` path
217
+ * (`enableChunking`) — static and lifecycle-independent, like `supportsArrow`;
218
+ * the per-bridge negotiated fact lives on {@link BridgeInfo}'s `transport` block.
219
+ * HTTP and Pyodide stay `false`. `supportsStreaming` is `false` on every backend
220
+ * (not implemented in 0.8.0). See docs/transport-capabilities.md for the full
221
+ * matrix.
222
+ */
223
+ export interface TransportCapabilities {
224
+ /** Which backend this transport drives. */
225
+ readonly backend: 'subprocess' | 'http' | 'pyodide';
226
+
227
+ /**
228
+ * Whether the transport can carry Arrow-encoded payloads (binary IPC frames)
229
+ * on the wire. Pyodide is JSON-only (pyarrow is unavailable in WASM), so it is
230
+ * `false` there; subprocess and HTTP can move Arrow bytes.
231
+ */
232
+ readonly supportsArrow: boolean;
233
+
234
+ /**
235
+ * Whether the transport can carry arbitrary binary data (e.g. Python `bytes`).
236
+ * All current backends carry binary via base64 envelopes, so this is `true`
237
+ * everywhere.
238
+ */
239
+ readonly supportsBinary: boolean;
240
+
241
+ /**
242
+ * Whether the transport splits a single logical message across multiple wire
243
+ * frames. Not implemented on any backend yet (planned for 0.8.0) — always
244
+ * `false`.
245
+ */
246
+ readonly supportsChunking: boolean;
247
+
248
+ /**
249
+ * Whether the transport can stream incremental results for a single request.
250
+ * Not implemented on any backend yet (planned for 0.8.0) — always `false`.
251
+ */
252
+ readonly supportsStreaming: boolean;
253
+
254
+ /**
255
+ * Maximum size, in bytes, of a single wire frame the transport will accept.
256
+ * `Number.POSITIVE_INFINITY` means the transport imposes no frame ceiling of
257
+ * its own (a higher layer — e.g. the codec's payload limit — may still cap the
258
+ * size). For the subprocess backend this is the JSONL line-length limit.
259
+ */
260
+ readonly maxFrameBytes: number;
261
+ }
262
+
105
263
  // =============================================================================
106
264
  // TRANSPORT INTERFACE
107
265
  // =============================================================================
@@ -177,6 +335,16 @@ export interface Transport extends Disposable {
177
335
  * Returns `false` in all other states.
178
336
  */
179
337
  readonly isReady: boolean;
338
+
339
+ /**
340
+ * Static, transport-level capability descriptor.
341
+ *
342
+ * Returns what this transport can carry and how it frames messages, with
343
+ * honest values for the transport's current behavior (see
344
+ * {@link TransportCapabilities}). It does NOT depend on lifecycle state — it is
345
+ * safe to call before `init()` and after `dispose()`.
346
+ */
347
+ capabilities(): TransportCapabilities;
180
348
  }
181
349
 
182
350
  // =============================================================================
@@ -228,6 +396,7 @@ export function isTransport(value: unknown): value is Transport {
228
396
  typeof (value as Transport).init === 'function' &&
229
397
  typeof (value as Transport).send === 'function' &&
230
398
  typeof (value as Transport).dispose === 'function' &&
399
+ typeof (value as Transport).capabilities === 'function' &&
231
400
  'isReady' in value
232
401
  );
233
402
  }
@@ -13,6 +13,16 @@ export interface PythonModule {
13
13
  exports: string[];
14
14
  }
15
15
 
16
+ /**
17
+ * How a callable is bound on its owning class.
18
+ *
19
+ * Mirrors `IRFunction.method_kind` from the Python IR (tywrap_ir 0.3.0):
20
+ * `'instance'` (default, keeps `self`), `'class'` (keeps `cls`), or
21
+ * `'static'` (no implicit first parameter). Module-level functions retain the
22
+ * `'instance'` default; it is only meaningful for class members.
23
+ */
24
+ export type PythonMethodKind = 'instance' | 'class' | 'static';
25
+
16
26
  export interface PythonFunction {
17
27
  name: string;
18
28
  signature: FunctionSignature;
@@ -23,6 +33,28 @@ export interface PythonFunction {
23
33
  typeParameters?: PythonGenericParameter[];
24
34
  returnType: PythonType;
25
35
  parameters: Parameter[];
36
+ /**
37
+ * Binding of this callable on its owning class. Defaults to `'instance'`.
38
+ * @see PythonMethodKind
39
+ */
40
+ methodKind?: PythonMethodKind;
41
+ }
42
+
43
+ /**
44
+ * A `@property` or `functools.cached_property` exposed on a class.
45
+ *
46
+ * Mirrors `IRAccessor` from the Python IR (tywrap_ir 0.3.0). Distinct from
47
+ * {@link PythonClass.properties} (which model TypedDict/NamedTuple/dataclass
48
+ * data shapes): accessors are computed attributes backed by a getter.
49
+ */
50
+ export interface PythonAccessor {
51
+ name: string;
52
+ type: PythonType;
53
+ docstring?: string;
54
+ /** True when there is no setter. `undefined` when undeterminable. */
55
+ readOnly?: boolean;
56
+ /** True for `functools.cached_property`. */
57
+ isCached: boolean;
26
58
  }
27
59
 
28
60
  export interface PythonClass {
@@ -30,6 +62,11 @@ export interface PythonClass {
30
62
  bases: string[];
31
63
  methods: PythonFunction[];
32
64
  properties: Property[];
65
+ /**
66
+ * `@property` / `functools.cached_property` accessors, emitted as TS getters.
67
+ * @see PythonAccessor
68
+ */
69
+ accessors?: PythonAccessor[];
33
70
  docstring?: string;
34
71
  decorators: string[];
35
72
  kind?: 'class' | 'protocol' | 'typed_dict' | 'namedtuple' | 'dataclass' | 'pydantic';
@@ -376,6 +413,25 @@ export interface TypeMappingConfig {
376
413
  /** Known bridge backends. Each speaks the identical "tywrap/1" protocol. */
377
414
  export type BridgeBackend = 'python-subprocess' | 'pyodide' | 'http';
378
415
 
416
+ /**
417
+ * Optional chunked-transport negotiation block in {@link BridgeInfo}.
418
+ *
419
+ * Reported by a bridge that understands the `tywrap-frame/1` framing protocol
420
+ * (subprocess only, 0.8.0). It lets the JS side learn, via the `meta` probe,
421
+ * whether the bridge can reassemble chunked frames and the maximum single-frame
422
+ * size it will accept. Absent on old bridges (and on HTTP/Pyodide, which stay
423
+ * single-frame in 0.8.0) — absence means "no chunking", which is backward
424
+ * compatible. See docs/transport-framing.md.
425
+ */
426
+ export interface BridgeTransportInfo {
427
+ /** Framing protocol the bridge speaks (e.g. `'tywrap-frame/1'`). */
428
+ frameProtocol: string;
429
+ /** Whether the bridge can fragment/reassemble chunked frames. */
430
+ supportsChunking: boolean;
431
+ /** Maximum size, in bytes, of a single wire frame the bridge will accept. */
432
+ maxFrameBytes: number;
433
+ }
434
+
379
435
  export interface BridgeInfo {
380
436
  protocol: string;
381
437
  protocolVersion: number;
@@ -389,6 +445,12 @@ export interface BridgeInfo {
389
445
  torchAvailable: boolean;
390
446
  sklearnAvailable: boolean;
391
447
  instances: number;
448
+ /**
449
+ * Optional chunked-transport negotiation block. Present only when the bridge
450
+ * advertises `tywrap-frame/1` framing; absent on old bridges and on
451
+ * HTTP/Pyodide (single-frame in 0.8.0). See {@link BridgeTransportInfo}.
452
+ */
453
+ transport?: BridgeTransportInfo;
392
454
  }
393
455
 
394
456
  // Analysis and generation results