tywrap 0.8.0 → 0.10.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 +42 -22
- package/SECURITY.md +38 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +2 -1
- package/dist/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +6 -3
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/generator.d.ts +23 -4
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +254 -170
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +3 -2
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +5 -5
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/base-bridge.d.ts +3 -7
- package/dist/runtime/base-bridge.d.ts.map +1 -1
- package/dist/runtime/base-bridge.js +4 -17
- package/dist/runtime/base-bridge.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +3 -22
- package/dist/runtime/bounded-context.d.ts.map +1 -1
- package/dist/runtime/bounded-context.js +13 -53
- package/dist/runtime/bounded-context.js.map +1 -1
- package/dist/runtime/bridge-codec.d.ts +3 -3
- package/dist/runtime/bridge-codec.d.ts.map +1 -1
- package/dist/runtime/bridge-codec.js +129 -62
- package/dist/runtime/bridge-codec.js.map +1 -1
- package/dist/runtime/errors.d.ts +16 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +17 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-transport.d.ts +1 -1
- package/dist/runtime/http-transport.d.ts.map +1 -1
- package/dist/runtime/http-transport.js +1 -1
- package/dist/runtime/http-transport.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +7 -36
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +2 -80
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +120 -59
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +345 -78
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
- package/dist/runtime/pyodide-transport.d.ts +1 -1
- package/dist/runtime/pyodide-transport.d.ts.map +1 -1
- package/dist/runtime/pyodide-transport.js +2 -3
- package/dist/runtime/pyodide-transport.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +7 -36
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +20 -102
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/subprocess-transport.d.ts +30 -69
- package/dist/runtime/subprocess-transport.d.ts.map +1 -1
- package/dist/runtime/subprocess-transport.js +179 -236
- package/dist/runtime/subprocess-transport.js.map +1 -1
- package/dist/runtime/timed-out-request-tracker.d.ts +2 -1
- package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -1
- package/dist/runtime/transport.d.ts +9 -19
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +1 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +50 -0
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +154 -0
- package/dist/runtime/validators.js.map +1 -1
- package/dist/types/index.d.ts +14 -39
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +3 -2
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +140 -13
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts +3 -16
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/codec.d.ts +17 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +500 -85
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +2 -1
- package/dist/utils/ir-cache.d.ts.map +1 -1
- package/dist/utils/runtime.d.ts +0 -29
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +16 -107
- package/dist/utils/runtime.js.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +7 -6
- package/runtime/frame_codec.py +7 -1
- package/runtime/python_bridge.py +85 -125
- package/runtime/tywrap_bridge_core.py +515 -187
- package/src/config/index.ts +11 -0
- package/src/core/annotation-parser.ts +7 -4
- package/src/core/generator.ts +330 -205
- package/src/core/mapper.ts +8 -8
- package/src/index.ts +7 -4
- package/src/runtime/base-bridge.ts +5 -30
- package/src/runtime/bounded-context.ts +12 -67
- package/src/runtime/bridge-codec.ts +158 -74
- package/src/runtime/errors.ts +21 -0
- package/src/runtime/http-transport.ts +6 -1
- package/src/runtime/index.ts +6 -0
- package/src/runtime/node.ts +9 -120
- package/src/runtime/pooled-transport.ts +424 -90
- package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
- package/src/runtime/pyodide-transport.ts +7 -3
- package/src/runtime/rpc-client.ts +37 -139
- package/src/runtime/subprocess-transport.ts +210 -285
- package/src/runtime/timed-out-request-tracker.ts +1 -1
- package/src/runtime/transport.ts +15 -23
- package/src/runtime/validators.ts +212 -0
- package/src/types/index.ts +21 -60
- package/src/tywrap.ts +157 -22
- package/src/utils/cache.ts +3 -3
- package/src/utils/codec.ts +749 -112
- package/src/utils/ir-cache.ts +1 -1
- package/src/utils/runtime.ts +17 -128
- package/src/version.ts +1 -1
- package/dist/core/discovery.d.ts +0 -103
- package/dist/core/discovery.d.ts.map +0 -1
- package/dist/core/discovery.js +0 -380
- package/dist/core/discovery.js.map +0 -1
- package/dist/core/validation.d.ts +0 -102
- package/dist/core/validation.d.ts.map +0 -1
- package/dist/core/validation.js +0 -490
- package/dist/core/validation.js.map +0 -1
- package/dist/runtime/base.d.ts +0 -22
- package/dist/runtime/base.d.ts.map +0 -1
- package/dist/runtime/base.js +0 -23
- package/dist/runtime/base.js.map +0 -1
- package/dist/runtime/transport-pool.d.ts +0 -196
- package/dist/runtime/transport-pool.d.ts.map +0 -1
- package/dist/runtime/transport-pool.js +0 -418
- package/dist/runtime/transport-pool.js.map +0 -1
- package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
- package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/runtime/safe_codec.py +0 -352
- package/src/core/discovery.ts +0 -477
- package/src/core/validation.ts +0 -729
- package/src/runtime/base.ts +0 -24
- package/src/runtime/transport-pool.ts +0 -538
package/runtime/python_bridge.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Reference tywrap Python
|
|
3
|
+
Reference tywrap Python subprocess bridge.
|
|
4
|
+
|
|
5
|
+
This module implements only the stdin/stdout subprocess loop for the Node/Bun/
|
|
6
|
+
Deno transport. `HttpBridge` connects to an HTTP server provided by the
|
|
7
|
+
operator; tywrap does not ship that server or an HTTP transport in this module.
|
|
8
|
+
The operator is responsible for securing any network exposure.
|
|
4
9
|
|
|
5
10
|
This module owns the I/O concerns and process identity for the Node/Bun/Deno
|
|
6
|
-
subprocess transport
|
|
11
|
+
subprocess transport:
|
|
7
12
|
- the stdin/stdout JSONL request/response loop (main())
|
|
8
13
|
- env-var size guards (TYWRAP_CODEC_MAX_BYTES / TYWRAP_REQUEST_MAX_BYTES)
|
|
9
14
|
- TYWRAP_CODEC_FALLBACK=json marker mode and TYWRAP_TORCH_ALLOW_COPY
|
|
@@ -14,7 +19,7 @@ subprocess transport and the HTTP transport:
|
|
|
14
19
|
the explicit byte-size limit error message)
|
|
15
20
|
|
|
16
21
|
SECURITY / TRUST MODEL: the bridge imports the requested module and getattrs the
|
|
17
|
-
requested function/class
|
|
22
|
+
requested function/class method, so call is an arbitrary
|
|
18
23
|
import+getattr+call surface. Two guards (implemented in tywrap_bridge_core) bound
|
|
19
24
|
that surface:
|
|
20
25
|
* TYWRAP_ALLOWED_MODULES (comma/space separated): when set, only those modules
|
|
@@ -37,10 +42,8 @@ import json
|
|
|
37
42
|
import os
|
|
38
43
|
import importlib # noqa: F401 (re-exported for compat / used by handlers via core)
|
|
39
44
|
|
|
40
|
-
from safe_codec import BridgeCodec, CodecError
|
|
41
|
-
|
|
42
45
|
import tywrap_bridge_core as core
|
|
43
|
-
from frame_codec import
|
|
46
|
+
from frame_codec import FrameError, Reassembler, encode_frames
|
|
44
47
|
|
|
45
48
|
# Re-export the shared protocol/serialization surface so existing importers of
|
|
46
49
|
# python_bridge keep working after the extraction (codex-flagged: runtime/ ships).
|
|
@@ -48,8 +51,9 @@ from tywrap_bridge_core import ( # noqa: F401
|
|
|
48
51
|
PROTOCOL,
|
|
49
52
|
PROTOCOL_VERSION,
|
|
50
53
|
CODEC_VERSION,
|
|
54
|
+
BridgeCodec,
|
|
55
|
+
CodecError,
|
|
51
56
|
ProtocolError,
|
|
52
|
-
InstanceHandleError,
|
|
53
57
|
ImportNotAllowedError,
|
|
54
58
|
AttributeNotAllowedError,
|
|
55
59
|
import_allowed_module,
|
|
@@ -90,8 +94,6 @@ except (OSError, ValueError, TypeError, AttributeError) as exc:
|
|
|
90
94
|
except (OSError, ValueError):
|
|
91
95
|
pass
|
|
92
96
|
|
|
93
|
-
instances = {}
|
|
94
|
-
|
|
95
97
|
FALLBACK_JSON = os.environ.get('TYWRAP_CODEC_FALLBACK', '').lower() == 'json'
|
|
96
98
|
TORCH_ALLOW_COPY = os.environ.get('TYWRAP_TORCH_ALLOW_COPY', '').lower() in ('1', 'true', 'yes')
|
|
97
99
|
BRIDGE_NAME = 'python-subprocess'
|
|
@@ -222,70 +224,22 @@ def get_request_max_bytes():
|
|
|
222
224
|
REQUEST_MAX_BYTES = get_request_max_bytes()
|
|
223
225
|
|
|
224
226
|
|
|
225
|
-
|
|
226
|
-
"""
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
def get_transport_frame_bytes():
|
|
228
|
+
"""Read the private frame ceiling argument supplied by SubprocessTransport."""
|
|
229
|
+
if "--tywrap-max-frame-bytes" not in sys.argv:
|
|
230
|
+
return 100 * 1024 * 1024
|
|
231
|
+
try:
|
|
232
|
+
index = sys.argv.index("--tywrap-max-frame-bytes")
|
|
233
|
+
value = int(sys.argv[index + 1])
|
|
234
|
+
except (ValueError, IndexError) as exc:
|
|
235
|
+
raise CodecConfigError("--tywrap-max-frame-bytes must be a positive integer") from exc
|
|
236
|
+
if value <= 0:
|
|
237
|
+
raise CodecConfigError("--tywrap-max-frame-bytes must be a positive integer")
|
|
238
|
+
return value
|
|
231
239
|
|
|
232
|
-
def negotiate_chunking():
|
|
233
|
-
"""
|
|
234
|
-
Resolve the chunked-transport (``tywrap-frame/1``) negotiation from env.
|
|
235
240
|
|
|
236
|
-
|
|
237
|
-
(see docs/transport-framing.md):
|
|
241
|
+
MAX_FRAME_BYTES = get_transport_frame_bytes()
|
|
238
242
|
|
|
239
|
-
* ``TYWRAP_TRANSPORT_CHUNKING=1`` -- enable framing
|
|
240
|
-
* ``TYWRAP_TRANSPORT_FRAME_PROTOCOL`` -- must equal ``tywrap-frame/1``
|
|
241
|
-
* ``TYWRAP_TRANSPORT_MAX_FRAME_BYTES`` -- the JS-side JSONL line ceiling
|
|
242
|
-
|
|
243
|
-
Returns ``(enabled, max_frame_bytes)``. Chunking is enabled ONLY when all
|
|
244
|
-
three agree: the flag is truthy, the advertised frame protocol matches the
|
|
245
|
-
one this bridge implements, and the max-frame size is a positive integer.
|
|
246
|
-
A mismatched frame protocol (a future framing version this bridge does not
|
|
247
|
-
speak) leaves chunking disabled -- the bridge then advertises
|
|
248
|
-
``supportsChunking: false`` and oversize responses fail LOUD (no silent
|
|
249
|
-
single-frame fallback), exactly as an old un-negotiated bridge would.
|
|
250
|
-
|
|
251
|
-
:raises TransportFrameBytesParseError: if the flag/protocol are set to enable
|
|
252
|
-
chunking but the max-frame-bytes value is not a positive integer.
|
|
253
|
-
"""
|
|
254
|
-
flag = os.environ.get('TYWRAP_TRANSPORT_CHUNKING', '').lower() in ('1', 'true', 'yes')
|
|
255
|
-
if not flag:
|
|
256
|
-
return False, None
|
|
257
|
-
frame_protocol = os.environ.get('TYWRAP_TRANSPORT_FRAME_PROTOCOL', '')
|
|
258
|
-
if frame_protocol != FRAME_PROTOCOL_ID:
|
|
259
|
-
# A framing protocol this bridge does not implement: stay single-frame.
|
|
260
|
-
return False, None
|
|
261
|
-
raw = os.environ.get('TYWRAP_TRANSPORT_MAX_FRAME_BYTES', '')
|
|
262
|
-
raw = str(raw).strip()
|
|
263
|
-
try:
|
|
264
|
-
value = int(raw)
|
|
265
|
-
except Exception as exc:
|
|
266
|
-
raise TransportFrameBytesParseError() from exc
|
|
267
|
-
if value <= 0:
|
|
268
|
-
raise TransportFrameBytesParseError()
|
|
269
|
-
return True, value
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
# Why: parse once at startup. CHUNKING_ENABLED gates the chunked write path and
|
|
273
|
-
# the transport block advertised in meta; MAX_FRAME_BYTES is the negotiated
|
|
274
|
-
# per-frame UTF-8 byte ceiling.
|
|
275
|
-
CHUNKING_ENABLED, MAX_FRAME_BYTES = negotiate_chunking()
|
|
276
|
-
|
|
277
|
-
# The transport negotiation block echoed back in the `meta` response so the JS
|
|
278
|
-
# side learns this bridge can reassemble chunked frames. None => omitted (an old
|
|
279
|
-
# bridge / un-negotiated process is indistinguishable on the wire).
|
|
280
|
-
TRANSPORT_INFO = (
|
|
281
|
-
{
|
|
282
|
-
'frameProtocol': FRAME_PROTOCOL_ID,
|
|
283
|
-
'supportsChunking': True,
|
|
284
|
-
'maxFrameBytes': MAX_FRAME_BYTES,
|
|
285
|
-
}
|
|
286
|
-
if CHUNKING_ENABLED
|
|
287
|
-
else None
|
|
288
|
-
)
|
|
289
243
|
|
|
290
244
|
|
|
291
245
|
def serialize(obj):
|
|
@@ -329,16 +283,14 @@ def handle_meta():
|
|
|
329
283
|
real pid and bridge='python-subprocess'.
|
|
330
284
|
"""
|
|
331
285
|
return core.build_meta(
|
|
332
|
-
instances,
|
|
333
286
|
bridge=BRIDGE_NAME,
|
|
334
287
|
pid=os.getpid(),
|
|
335
288
|
python_version=sys.version.split()[0],
|
|
336
289
|
codec_fallback='json' if FALLBACK_JSON else 'none',
|
|
337
|
-
transport_info=TRANSPORT_INFO,
|
|
338
290
|
)
|
|
339
291
|
|
|
340
292
|
|
|
341
|
-
def dispatch_request(msg):
|
|
293
|
+
def dispatch_request(msg, *, has_envelope_markers=True):
|
|
342
294
|
"""
|
|
343
295
|
Dispatch a validated request to the correct handler (subprocess identity).
|
|
344
296
|
|
|
@@ -348,7 +300,6 @@ def dispatch_request(msg):
|
|
|
348
300
|
"""
|
|
349
301
|
out = core.dispatch_request(
|
|
350
302
|
msg,
|
|
351
|
-
instances,
|
|
352
303
|
bridge=BRIDGE_NAME,
|
|
353
304
|
pid=os.getpid(),
|
|
354
305
|
force_json_markers=FALLBACK_JSON,
|
|
@@ -357,7 +308,7 @@ def dispatch_request(msg):
|
|
|
357
308
|
torch_allow_copy=TORCH_ALLOW_COPY,
|
|
358
309
|
allowed_modules=ALLOWED_MODULES,
|
|
359
310
|
allow_private_attrs=ALLOW_PRIVATE_ATTRS,
|
|
360
|
-
|
|
311
|
+
has_envelope_markers=has_envelope_markers,
|
|
361
312
|
)
|
|
362
313
|
return out['id'], out['result']
|
|
363
314
|
|
|
@@ -374,10 +325,11 @@ def encode_response(out):
|
|
|
374
325
|
except CodecError as exc:
|
|
375
326
|
# Convert CodecError to ValueError for consistent error handling
|
|
376
327
|
raise ValueError(str(exc)) from exc
|
|
377
|
-
|
|
328
|
+
payload_utf8 = payload.encode('utf-8')
|
|
329
|
+
payload_bytes = len(payload_utf8)
|
|
378
330
|
if CODEC_MAX_BYTES is not None and payload_bytes > CODEC_MAX_BYTES:
|
|
379
331
|
raise PayloadTooLargeError(payload_bytes, CODEC_MAX_BYTES)
|
|
380
|
-
return payload
|
|
332
|
+
return payload, payload_utf8
|
|
381
333
|
|
|
382
334
|
|
|
383
335
|
def write_payload(payload: str) -> bool:
|
|
@@ -395,22 +347,17 @@ def write_payload(payload: str) -> bool:
|
|
|
395
347
|
return False
|
|
396
348
|
|
|
397
349
|
|
|
398
|
-
def write_response(payload: str, response_id) -> bool:
|
|
350
|
+
def write_response(payload: str, payload_utf8: bytes, response_id) -> bool:
|
|
399
351
|
"""
|
|
400
352
|
Write a fully-encoded JSONL response, fragmenting it into ``tywrap-frame/1``
|
|
401
|
-
frames when
|
|
402
|
-
ceiling.
|
|
353
|
+
frames when the payload exceeds the fixed per-frame ceiling.
|
|
403
354
|
|
|
404
355
|
Why: a single response can exceed the JS-side JSONL line ceiling
|
|
405
|
-
(``maxLineLength``).
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
response when chunking was not negotiated) keep going out as one JSONL line
|
|
411
|
-
exactly as before. There is NO silent single-frame fallback for an oversize
|
|
412
|
-
response on an un-negotiated bridge: it is written whole and the JS line
|
|
413
|
-
ceiling rejects it LOUD, by design.
|
|
356
|
+
(``maxLineLength``). The packaged subprocess transport always enables
|
|
357
|
+
framing, so an oversize response is split into frames, each written as its
|
|
358
|
+
own JSONL line and flushed one at a time so the OS pipe provides
|
|
359
|
+
backpressure. The TS reassembler rebuilds the logical response before the
|
|
360
|
+
codec sees it. Small responses keep the single-line fast path.
|
|
414
361
|
|
|
415
362
|
Frames require an integer correlation id. A response with a non-integer id
|
|
416
363
|
(only reachable on a malformed request whose error envelope carries id=None)
|
|
@@ -419,10 +366,7 @@ def write_response(payload: str, response_id) -> bool:
|
|
|
419
366
|
Returns False if the parent's stdin/our stdout closed mid-write (BrokenPipe),
|
|
420
367
|
so the caller can exit the loop cleanly.
|
|
421
368
|
"""
|
|
422
|
-
|
|
423
|
-
return write_payload(payload)
|
|
424
|
-
|
|
425
|
-
payload_bytes = len(payload.encode('utf-8'))
|
|
369
|
+
payload_bytes = len(payload_utf8)
|
|
426
370
|
if payload_bytes <= MAX_FRAME_BYTES:
|
|
427
371
|
return write_payload(payload)
|
|
428
372
|
|
|
@@ -437,6 +381,7 @@ def write_response(payload: str, response_id) -> bool:
|
|
|
437
381
|
id=response_id,
|
|
438
382
|
stream='response',
|
|
439
383
|
max_frame_bytes=MAX_FRAME_BYTES,
|
|
384
|
+
total_bytes=payload_bytes,
|
|
440
385
|
)
|
|
441
386
|
for frame in frames:
|
|
442
387
|
# One frame per JSONL line; flush per frame so the pipe backpressures
|
|
@@ -447,15 +392,24 @@ def write_response(payload: str, response_id) -> bool:
|
|
|
447
392
|
|
|
448
393
|
|
|
449
394
|
# Reassembles `tywrap-frame/1` REQUEST frames (W5) back into a single logical
|
|
450
|
-
# request line.
|
|
451
|
-
#
|
|
452
|
-
# to the 'request' stream. No reassembly-bytes cap here: the request size is
|
|
395
|
+
# request line. Framing is always accepted and restricted to the 'request'
|
|
396
|
+
# stream. No reassembly-bytes cap here: the request size is
|
|
453
397
|
# already bounded by the TS codec's maxPayloadBytes on the sending side, and
|
|
454
398
|
# TYWRAP_REQUEST_MAX_BYTES is enforced on the complete payload after reassembly
|
|
455
399
|
# (process_request_line), preserving the W5 post-reassembly semantics.
|
|
456
|
-
_request_reassembler = (
|
|
457
|
-
|
|
458
|
-
|
|
400
|
+
_request_reassembler = Reassembler(expected_stream='request')
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _accept_request_frame(frame):
|
|
404
|
+
"""Accept one request frame, abandoning a different older serialized burst."""
|
|
405
|
+
frame_id = frame.get('id')
|
|
406
|
+
if frame.get('seq') == 0 and not _request_reassembler.is_pending(frame_id):
|
|
407
|
+
# The stdin loop is deliberately blocking/serial: an abandoned partial
|
|
408
|
+
# buffer persists while the worker is idle, then is freed by the next
|
|
409
|
+
# logical request (or by process restart). A background TTL timer would
|
|
410
|
+
# add concurrency solely to reclaim memory during an otherwise idle loop.
|
|
411
|
+
_request_reassembler.clear_pending()
|
|
412
|
+
return _request_reassembler.accept(frame)
|
|
459
413
|
|
|
460
414
|
|
|
461
415
|
def _try_parse_frame_line(line):
|
|
@@ -463,7 +417,7 @@ def _try_parse_frame_line(line):
|
|
|
463
417
|
Return a frame dict if ``line`` is a ``tywrap-frame/1`` envelope, else None.
|
|
464
418
|
|
|
465
419
|
Why: a request frame is a JSON object carrying ``__tywrap_frame__``. We only
|
|
466
|
-
treat a line as a frame when
|
|
420
|
+
treat a line as a frame when it parses as such an
|
|
467
421
|
object; anything else (including invalid JSON) falls through to the normal
|
|
468
422
|
single-line request path, which reports the JSON error exactly as before.
|
|
469
423
|
Structural validity (protocol, seq/total ranges, etc.) is enforced by the
|
|
@@ -504,7 +458,8 @@ def process_request_line(line):
|
|
|
504
458
|
# Why: preserve request ids even when handlers raise.
|
|
505
459
|
mid = req_id
|
|
506
460
|
try:
|
|
507
|
-
|
|
461
|
+
has_envelope_markers = '__tywrap' in line or '__type__' in line
|
|
462
|
+
mid, result = dispatch_request(msg, has_envelope_markers=has_envelope_markers)
|
|
508
463
|
out = {'id': mid, 'protocol': PROTOCOL, 'result': result}
|
|
509
464
|
except ProtocolError as e:
|
|
510
465
|
emit_protocol_diagnostic(str(e))
|
|
@@ -523,11 +478,11 @@ def process_request_line(line):
|
|
|
523
478
|
out = build_error_payload(mid, e, include_traceback=False)
|
|
524
479
|
|
|
525
480
|
try:
|
|
526
|
-
payload = encode_response(out)
|
|
481
|
+
payload, payload_utf8 = encode_response(out)
|
|
527
482
|
# Correlate frames by the response id when chunking; out always
|
|
528
483
|
# carries the request id (or None for a malformed-request envelope).
|
|
529
484
|
response_id = out.get('id') if isinstance(out, dict) else None
|
|
530
|
-
if not write_response(payload, response_id):
|
|
485
|
+
if not write_response(payload, payload_utf8, response_id):
|
|
531
486
|
return False
|
|
532
487
|
except Exception as e: # noqa: BLE001
|
|
533
488
|
# Why: fallback error keeps responses well-formed even if serialization fails.
|
|
@@ -548,30 +503,35 @@ def main():
|
|
|
548
503
|
if not line:
|
|
549
504
|
continue
|
|
550
505
|
|
|
551
|
-
#
|
|
552
|
-
#
|
|
553
|
-
#
|
|
554
|
-
#
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
return
|
|
506
|
+
# A line may be a `tywrap-frame/1` REQUEST frame. Reassemble per id; only
|
|
507
|
+
# the completed logical request is handed to process_request_line (which
|
|
508
|
+
# then enforces the request-size guard on the reassembled payload).
|
|
509
|
+
# Non-frame lines are normal requests.
|
|
510
|
+
frame = _try_parse_frame_line(line)
|
|
511
|
+
if frame is not None:
|
|
512
|
+
# JS serializes request frame bursts. A seq=0 frame for a different
|
|
513
|
+
# id proves any older partial stream was abandoned; a duplicate
|
|
514
|
+
# seq=0 for the same id must still fail loud in the reassembler.
|
|
515
|
+
try:
|
|
516
|
+
reassembled = _accept_request_frame(frame)
|
|
517
|
+
except FrameError as exc:
|
|
518
|
+
# A framing-protocol violation desyncs the request stream and
|
|
519
|
+
# there is no correlatable response to write (the id may be
|
|
520
|
+
# malformed). Fail LOUD on stderr and stop the loop so the
|
|
521
|
+
# transport restarts the bridge rather than silently
|
|
522
|
+
# mis-parsing subsequent frames.
|
|
523
|
+
emit_protocol_diagnostic(f'Request frame error: {exc}')
|
|
524
|
+
return
|
|
525
|
+
if reassembled is None:
|
|
526
|
+
# More frames needed for this id; await the rest.
|
|
573
527
|
continue
|
|
528
|
+
if not process_request_line(reassembled):
|
|
529
|
+
return
|
|
530
|
+
continue
|
|
574
531
|
|
|
532
|
+
# Same idle-retention tradeoff as _accept_request_frame: a partial burst
|
|
533
|
+
# is retained only until the next request or process restart.
|
|
534
|
+
_request_reassembler.clear_pending()
|
|
575
535
|
if not process_request_line(line):
|
|
576
536
|
return
|
|
577
537
|
|