tywrap 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/runtime/base.d.ts +17 -7
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +18 -1
- package/dist/runtime/base.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +252 -0
- package/dist/runtime/bounded-context.d.ts.map +1 -0
- package/dist/runtime/bounded-context.js +454 -0
- package/dist/runtime/bounded-context.js.map +1 -0
- package/dist/runtime/bridge-protocol.d.ts +167 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -0
- package/dist/runtime/bridge-protocol.js +247 -0
- package/dist/runtime/bridge-protocol.js.map +1 -0
- package/dist/runtime/disposable.d.ts +40 -0
- package/dist/runtime/disposable.d.ts.map +1 -0
- package/dist/runtime/disposable.js +49 -0
- package/dist/runtime/disposable.js.map +1 -0
- package/dist/runtime/http-io.d.ts +91 -0
- package/dist/runtime/http-io.d.ts.map +1 -0
- package/dist/runtime/http-io.js +195 -0
- package/dist/runtime/http-io.js.map +1 -0
- package/dist/runtime/http.d.ts +47 -13
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +55 -74
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts +142 -28
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +321 -168
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +19 -125
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +19 -550
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +131 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -0
- package/dist/runtime/pooled-transport.js +175 -0
- package/dist/runtime/pooled-transport.js.map +1 -0
- package/dist/runtime/process-io.d.ts +204 -0
- package/dist/runtime/process-io.d.ts.map +1 -0
- package/dist/runtime/process-io.js +695 -0
- package/dist/runtime/process-io.js.map +1 -0
- package/dist/runtime/pyodide-io.d.ts +155 -0
- package/dist/runtime/pyodide-io.d.ts.map +1 -0
- package/dist/runtime/pyodide-io.js +397 -0
- package/dist/runtime/pyodide-io.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +51 -19
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +57 -186
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +81 -0
- package/dist/runtime/safe-codec.d.ts.map +1 -0
- package/dist/runtime/safe-codec.js +345 -0
- package/dist/runtime/safe-codec.js.map +1 -0
- package/dist/runtime/transport.d.ts +186 -0
- package/dist/runtime/transport.d.ts.map +1 -0
- package/dist/runtime/transport.js +86 -0
- package/dist/runtime/transport.js.map +1 -0
- package/dist/runtime/validators.d.ts +131 -0
- package/dist/runtime/validators.d.ts.map +1 -0
- package/dist/runtime/validators.js +219 -0
- package/dist/runtime/validators.js.map +1 -0
- package/dist/runtime/worker-pool.d.ts +196 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -0
- package/dist/runtime/worker-pool.js +371 -0
- package/dist/runtime/worker-pool.js.map +1 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +120 -1
- package/dist/utils/codec.js.map +1 -1
- package/package.json +2 -2
- package/runtime/python_bridge.py +30 -3
- package/runtime/safe_codec.py +344 -0
- package/src/index.ts +48 -5
- package/src/runtime/base.ts +18 -26
- package/src/runtime/bounded-context.ts +608 -0
- package/src/runtime/bridge-protocol.ts +319 -0
- package/src/runtime/disposable.ts +65 -0
- package/src/runtime/http-io.ts +244 -0
- package/src/runtime/http.ts +71 -117
- package/src/runtime/node.ts +460 -217
- package/src/runtime/optimized-node.ts +19 -761
- package/src/runtime/pooled-transport.ts +252 -0
- package/src/runtime/process-io.ts +902 -0
- package/src/runtime/pyodide-io.ts +485 -0
- package/src/runtime/pyodide.ts +75 -215
- package/src/runtime/safe-codec.ts +443 -0
- package/src/runtime/transport.ts +273 -0
- package/src/runtime/validators.ts +241 -0
- package/src/runtime/worker-pool.ts +498 -0
- package/src/utils/codec.ts +126 -1
package/runtime/python_bridge.py
CHANGED
|
@@ -11,6 +11,8 @@ import decimal
|
|
|
11
11
|
import uuid
|
|
12
12
|
from pathlib import Path, PurePath
|
|
13
13
|
|
|
14
|
+
from safe_codec import SafeCodec, CodecError
|
|
15
|
+
|
|
14
16
|
# Ensure the working directory is importable so local modules can be resolved when
|
|
15
17
|
# the bridge is launched as a script from a different directory.
|
|
16
18
|
try:
|
|
@@ -95,6 +97,16 @@ def get_codec_max_bytes():
|
|
|
95
97
|
# Why: parse once at startup to avoid per-response env lookups.
|
|
96
98
|
CODEC_MAX_BYTES = get_codec_max_bytes()
|
|
97
99
|
|
|
100
|
+
# Why: use SafeCodec for final JSON encoding to reject NaN/Infinity and handle
|
|
101
|
+
# edge cases like numpy scalars. We use sys.maxsize for SafeCodec's internal limit
|
|
102
|
+
# to preserve the original "no limit unless TYWRAP_CODEC_MAX_BYTES is set" behavior.
|
|
103
|
+
# The explicit size check in encode_response() provides the specific error message
|
|
104
|
+
# mentioning the env var name, which is important for debugging.
|
|
105
|
+
_response_codec = SafeCodec(
|
|
106
|
+
allow_nan=False,
|
|
107
|
+
max_payload_bytes=sys.maxsize,
|
|
108
|
+
)
|
|
109
|
+
|
|
98
110
|
|
|
99
111
|
def get_request_max_bytes():
|
|
100
112
|
"""
|
|
@@ -267,6 +279,11 @@ def serialize_ndarray(obj):
|
|
|
267
279
|
Why: Arrow IPC gives a compact, lossless binary payload that the JS side can decode as a
|
|
268
280
|
Table. If JSON fallback is explicitly requested, honor it even when pyarrow is installed so
|
|
269
281
|
callers don't unexpectedly need an Arrow decoder on the TypeScript side.
|
|
282
|
+
|
|
283
|
+
Note: PyArrow's pa.array() only handles 1D arrays. For multi-dimensional arrays, we flatten
|
|
284
|
+
before encoding and include shape metadata for reconstruction on the JS side. This maintains
|
|
285
|
+
Arrow's binary efficiency while working with the current arrow-js implementation (which
|
|
286
|
+
doesn't yet support FixedShapeTensorArray). See: https://github.com/apache/arrow-js/issues/115
|
|
270
287
|
"""
|
|
271
288
|
if FALLBACK_JSON:
|
|
272
289
|
return serialize_ndarray_json(obj)
|
|
@@ -277,7 +294,11 @@ def serialize_ndarray(obj):
|
|
|
277
294
|
'Arrow encoding unavailable for ndarray; install pyarrow or set TYWRAP_CODEC_FALLBACK=json to enable JSON fallback'
|
|
278
295
|
) from exc
|
|
279
296
|
try:
|
|
280
|
-
|
|
297
|
+
# Flatten multi-dimensional arrays for Arrow compatibility
|
|
298
|
+
# pa.array() only handles 1D arrays; we preserve shape for JS-side reconstruction
|
|
299
|
+
original_shape = list(obj.shape) if hasattr(obj, 'shape') else None
|
|
300
|
+
flat = obj.flatten() if hasattr(obj, 'ndim') and obj.ndim > 1 else obj
|
|
301
|
+
arr = pa.array(flat)
|
|
281
302
|
table = pa.Table.from_arrays([arr], names=['value'])
|
|
282
303
|
sink = pa.BufferOutputStream()
|
|
283
304
|
with pa.ipc.new_stream(sink, table.schema) as writer:
|
|
@@ -289,7 +310,8 @@ def serialize_ndarray(obj):
|
|
|
289
310
|
'codecVersion': CODEC_VERSION,
|
|
290
311
|
'encoding': 'arrow',
|
|
291
312
|
'b64': b64,
|
|
292
|
-
'shape':
|
|
313
|
+
'shape': original_shape,
|
|
314
|
+
'dtype': str(obj.dtype) if hasattr(obj, 'dtype') else None,
|
|
293
315
|
}
|
|
294
316
|
except Exception as exc:
|
|
295
317
|
if FALLBACK_JSON:
|
|
@@ -779,8 +801,13 @@ def encode_response(out):
|
|
|
779
801
|
Serialize the response and enforce size limits.
|
|
780
802
|
|
|
781
803
|
Why: keep payload size checks outside the main loop for clarity and lint compliance.
|
|
804
|
+
Uses SafeCodec to reject NaN/Infinity and handle edge cases like numpy scalars.
|
|
782
805
|
"""
|
|
783
|
-
|
|
806
|
+
try:
|
|
807
|
+
payload = _response_codec.encode(out)
|
|
808
|
+
except CodecError as exc:
|
|
809
|
+
# Convert CodecError to ValueError for consistent error handling
|
|
810
|
+
raise ValueError(str(exc)) from exc
|
|
784
811
|
payload_bytes = len(payload.encode('utf-8'))
|
|
785
812
|
if CODEC_MAX_BYTES is not None and payload_bytes > CODEC_MAX_BYTES:
|
|
786
813
|
raise PayloadTooLargeError(payload_bytes, CODEC_MAX_BYTES)
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Safe JSON codec with explicit edge case handling.
|
|
3
|
+
|
|
4
|
+
Provides bidirectional validation and serialization for the JS<->Python bridge.
|
|
5
|
+
This module handles special Python types and enforces payload size limits to
|
|
6
|
+
ensure predictable behavior at the language boundary.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
from datetime import date, datetime, time, timedelta
|
|
13
|
+
from decimal import Decimal
|
|
14
|
+
from pathlib import Path, PurePath
|
|
15
|
+
from typing import Any, Optional, Union
|
|
16
|
+
from uuid import UUID
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CodecError(Exception):
|
|
20
|
+
"""Raised when encoding/decoding fails."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_nan_or_inf(value: Any) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Check if a numeric value is NaN or Infinity.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
value: The value to check.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
True if the value is NaN, positive infinity, or negative infinity.
|
|
34
|
+
"""
|
|
35
|
+
if not isinstance(value, (int, float)):
|
|
36
|
+
return False
|
|
37
|
+
try:
|
|
38
|
+
return math.isnan(value) or math.isinf(value)
|
|
39
|
+
except (TypeError, ValueError):
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _is_numpy_scalar(obj: Any) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Detect numpy scalar types when NumPy is installed.
|
|
46
|
+
|
|
47
|
+
Why: numpy scalars have an `.item()` method to extract Python primitives,
|
|
48
|
+
which is needed for JSON serialization.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
import numpy as np
|
|
52
|
+
except ImportError:
|
|
53
|
+
return False
|
|
54
|
+
return isinstance(obj, (np.generic, np.ndarray)) and obj.ndim == 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_pandas_scalar(obj: Any) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Detect pandas scalar types (Timestamp, Timedelta, etc.).
|
|
60
|
+
|
|
61
|
+
Why: pandas wraps numpy scalars with additional metadata; extracting the
|
|
62
|
+
underlying value ensures clean JSON serialization.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
import pandas as pd
|
|
66
|
+
except ImportError:
|
|
67
|
+
return False
|
|
68
|
+
# Check for pandas Timestamp, Timedelta, NaT, etc.
|
|
69
|
+
return isinstance(
|
|
70
|
+
obj,
|
|
71
|
+
(
|
|
72
|
+
pd.Timestamp,
|
|
73
|
+
pd.Timedelta,
|
|
74
|
+
type(pd.NaT),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _has_pydantic_model_dump(obj: Any) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Check if object is a Pydantic v2 model with model_dump method.
|
|
82
|
+
|
|
83
|
+
Why: Pydantic models should be serialized via their built-in mechanism
|
|
84
|
+
to respect field aliases and serialization modes.
|
|
85
|
+
"""
|
|
86
|
+
model_dump = getattr(obj, 'model_dump', None)
|
|
87
|
+
return callable(model_dump)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class SafeCodec:
|
|
91
|
+
"""
|
|
92
|
+
Safe JSON codec with explicit edge case handling.
|
|
93
|
+
|
|
94
|
+
This codec provides:
|
|
95
|
+
- Rejection of NaN/Infinity by default (configurable)
|
|
96
|
+
- Payload size limits to prevent memory exhaustion
|
|
97
|
+
- Automatic handling of common Python types (datetime, Decimal, UUID, etc.)
|
|
98
|
+
- Support for numpy/pandas scalars
|
|
99
|
+
- Pydantic model serialization
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
allow_nan: If False (default), reject NaN/Infinity values.
|
|
103
|
+
max_payload_bytes: Maximum payload size in bytes (default 10MB).
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
>>> codec = SafeCodec()
|
|
107
|
+
>>> codec.encode({"key": "value"})
|
|
108
|
+
'{"key": "value"}'
|
|
109
|
+
>>> codec.decode('{"key": "value"}')
|
|
110
|
+
{'key': 'value'}
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
allow_nan: bool = False,
|
|
116
|
+
max_payload_bytes: int = 10 * 1024 * 1024,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Initialize the codec with configuration.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
allow_nan: If False (default), reject NaN/Infinity values.
|
|
123
|
+
max_payload_bytes: Maximum payload size in bytes (default 10MB).
|
|
124
|
+
"""
|
|
125
|
+
self.allow_nan = allow_nan
|
|
126
|
+
self.max_payload_bytes = max_payload_bytes
|
|
127
|
+
|
|
128
|
+
def encode(self, value: Any) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Encode a Python value to a JSON string.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
value: The Python value to encode.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
A JSON string representation of the value.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
CodecError: If encoding fails due to:
|
|
140
|
+
- NaN/Infinity values when allow_nan is False
|
|
141
|
+
- Payload exceeds max_payload_bytes
|
|
142
|
+
- Value contains non-serializable types
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
result = json.dumps(
|
|
146
|
+
value,
|
|
147
|
+
default=self._default_encoder,
|
|
148
|
+
allow_nan=self.allow_nan,
|
|
149
|
+
)
|
|
150
|
+
except ValueError as exc:
|
|
151
|
+
# json.dumps raises ValueError for NaN/Infinity when allow_nan=False
|
|
152
|
+
error_msg = str(exc).lower()
|
|
153
|
+
if 'nan' in error_msg or 'infinity' in error_msg or 'inf' in error_msg:
|
|
154
|
+
raise CodecError(
|
|
155
|
+
'Cannot serialize NaN - NaN/Infinity not allowed in JSON'
|
|
156
|
+
) from exc
|
|
157
|
+
raise CodecError(f'JSON encoding failed: {exc}') from exc
|
|
158
|
+
except TypeError as exc:
|
|
159
|
+
raise CodecError(f'JSON encoding failed: {exc}') from exc
|
|
160
|
+
|
|
161
|
+
# Check payload size
|
|
162
|
+
payload_bytes = len(result.encode('utf-8'))
|
|
163
|
+
if payload_bytes > self.max_payload_bytes:
|
|
164
|
+
raise CodecError(f'Payload exceeds {self.max_payload_bytes} bytes')
|
|
165
|
+
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
def decode(self, payload: str) -> Any:
|
|
169
|
+
"""
|
|
170
|
+
Decode a JSON string to a Python value.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
payload: The JSON string to decode.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
The decoded Python value.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
CodecError: If decoding fails due to:
|
|
180
|
+
- Payload exceeds max_payload_bytes
|
|
181
|
+
- Invalid JSON syntax
|
|
182
|
+
"""
|
|
183
|
+
# Check payload size first
|
|
184
|
+
payload_bytes = len(payload.encode('utf-8'))
|
|
185
|
+
if payload_bytes > self.max_payload_bytes:
|
|
186
|
+
raise CodecError(f'Payload exceeds {self.max_payload_bytes} bytes')
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
return json.loads(payload)
|
|
190
|
+
except json.JSONDecodeError as exc:
|
|
191
|
+
raise CodecError(f'JSON decoding failed: {exc}') from exc
|
|
192
|
+
|
|
193
|
+
def _default_encoder(self, obj: Any) -> Any:
|
|
194
|
+
"""
|
|
195
|
+
Handle special Python types during JSON encoding.
|
|
196
|
+
|
|
197
|
+
This method is called by json.dumps for objects that are not natively
|
|
198
|
+
JSON serializable.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
obj: The object to encode.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
A JSON-serializable representation of the object.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
TypeError: If the object cannot be serialized.
|
|
208
|
+
CodecError: If the object contains NaN/Infinity when not allowed.
|
|
209
|
+
"""
|
|
210
|
+
# Handle numpy/pandas scalars first (they need .item() extraction)
|
|
211
|
+
if _is_numpy_scalar(obj):
|
|
212
|
+
extracted = obj.item()
|
|
213
|
+
# Check for NaN/Infinity in extracted value
|
|
214
|
+
if not self.allow_nan and _is_nan_or_inf(extracted):
|
|
215
|
+
raise CodecError(
|
|
216
|
+
'Cannot serialize NaN - NaN/Infinity not allowed in JSON'
|
|
217
|
+
)
|
|
218
|
+
return extracted
|
|
219
|
+
|
|
220
|
+
if _is_pandas_scalar(obj):
|
|
221
|
+
try:
|
|
222
|
+
import pandas as pd
|
|
223
|
+
except ImportError:
|
|
224
|
+
pass
|
|
225
|
+
else:
|
|
226
|
+
# Handle NaT (Not a Time)
|
|
227
|
+
if obj is pd.NaT or (hasattr(pd, 'isna') and pd.isna(obj)):
|
|
228
|
+
return None
|
|
229
|
+
# Pandas Timestamp -> ISO string
|
|
230
|
+
if isinstance(obj, pd.Timestamp):
|
|
231
|
+
return obj.isoformat()
|
|
232
|
+
# Pandas Timedelta -> total seconds
|
|
233
|
+
if isinstance(obj, pd.Timedelta):
|
|
234
|
+
return obj.total_seconds()
|
|
235
|
+
|
|
236
|
+
# datetime types
|
|
237
|
+
if isinstance(obj, datetime):
|
|
238
|
+
return obj.isoformat()
|
|
239
|
+
|
|
240
|
+
if isinstance(obj, date):
|
|
241
|
+
return obj.isoformat()
|
|
242
|
+
|
|
243
|
+
if isinstance(obj, time):
|
|
244
|
+
return obj.isoformat()
|
|
245
|
+
|
|
246
|
+
# timedelta -> total seconds (consistent with python_bridge.py)
|
|
247
|
+
if isinstance(obj, timedelta):
|
|
248
|
+
return obj.total_seconds()
|
|
249
|
+
|
|
250
|
+
# Decimal -> string (preserves precision)
|
|
251
|
+
if isinstance(obj, Decimal):
|
|
252
|
+
return str(obj)
|
|
253
|
+
|
|
254
|
+
# UUID -> string
|
|
255
|
+
if isinstance(obj, UUID):
|
|
256
|
+
return str(obj)
|
|
257
|
+
|
|
258
|
+
# Path -> string
|
|
259
|
+
if isinstance(obj, (Path, PurePath)):
|
|
260
|
+
return str(obj)
|
|
261
|
+
|
|
262
|
+
# bytes/bytearray -> base64 with type marker
|
|
263
|
+
if isinstance(obj, (bytes, bytearray)):
|
|
264
|
+
return {
|
|
265
|
+
'__type__': 'bytes',
|
|
266
|
+
'encoding': 'base64',
|
|
267
|
+
'data': base64.b64encode(obj).decode('ascii'),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Pydantic models
|
|
271
|
+
if _has_pydantic_model_dump(obj):
|
|
272
|
+
try:
|
|
273
|
+
return obj.model_dump(by_alias=True, mode='json')
|
|
274
|
+
except TypeError:
|
|
275
|
+
# Older Pydantic versions may not support mode='json'
|
|
276
|
+
return obj.model_dump(by_alias=True)
|
|
277
|
+
|
|
278
|
+
# Sets -> lists (common conversion)
|
|
279
|
+
if isinstance(obj, (set, frozenset)):
|
|
280
|
+
return list(obj)
|
|
281
|
+
|
|
282
|
+
# Complex numbers (rejected by default as they contain floats)
|
|
283
|
+
if isinstance(obj, complex):
|
|
284
|
+
raise TypeError(
|
|
285
|
+
f'Object of type {type(obj).__name__} is not JSON serializable'
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Fallback: raise TypeError with clear message
|
|
289
|
+
raise TypeError(
|
|
290
|
+
f'Object of type {type(obj).__name__} is not JSON serializable'
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# Module-level convenience instance with default settings
|
|
295
|
+
_default_codec: Optional[SafeCodec] = None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_default_codec() -> SafeCodec:
|
|
299
|
+
"""
|
|
300
|
+
Get or create the default SafeCodec instance.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
The default SafeCodec instance with standard settings.
|
|
304
|
+
"""
|
|
305
|
+
global _default_codec
|
|
306
|
+
if _default_codec is None:
|
|
307
|
+
_default_codec = SafeCodec()
|
|
308
|
+
return _default_codec
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def encode(value: Any, *, allow_nan: bool = False) -> str:
|
|
312
|
+
"""
|
|
313
|
+
Convenience function to encode a value using default settings.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
value: The Python value to encode.
|
|
317
|
+
allow_nan: If True, allow NaN/Infinity values.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
A JSON string representation of the value.
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
CodecError: If encoding fails.
|
|
324
|
+
"""
|
|
325
|
+
if allow_nan:
|
|
326
|
+
codec = SafeCodec(allow_nan=True)
|
|
327
|
+
return codec.encode(value)
|
|
328
|
+
return get_default_codec().encode(value)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def decode(payload: str) -> Any:
|
|
332
|
+
"""
|
|
333
|
+
Convenience function to decode a JSON string using default settings.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
payload: The JSON string to decode.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
The decoded Python value.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
CodecError: If decoding fails.
|
|
343
|
+
"""
|
|
344
|
+
return get_default_codec().decode(payload)
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,50 @@ import { tywrap } from './tywrap.js';
|
|
|
9
9
|
|
|
10
10
|
export type { TywrapConfig } from './config/index.js';
|
|
11
11
|
export { defineConfig, resolveConfig } from './config/index.js';
|
|
12
|
+
// BoundedContext - unified abstraction for cross-boundary concerns
|
|
13
|
+
export { BoundedContext, type ContextState, type ExecuteOptions } from './runtime/bounded-context.js';
|
|
14
|
+
// BridgeProtocol - unified BoundedContext + SafeCodec + Transport
|
|
15
|
+
export { BridgeProtocol, type BridgeProtocolOptions } from './runtime/bridge-protocol.js';
|
|
16
|
+
// SafeCodec - validation and serialization for JS<->Python boundary
|
|
17
|
+
export { SafeCodec, type CodecOptions } from './runtime/safe-codec.js';
|
|
18
|
+
// Transport - abstract I/O channel interface
|
|
19
|
+
export type { Transport, TransportOptions, ProtocolMessage, ProtocolResponse } from './runtime/transport.js';
|
|
20
|
+
export { PROTOCOL_ID, isTransport, isProtocolMessage, isProtocolResponse } from './runtime/transport.js';
|
|
21
|
+
// Transport implementations
|
|
22
|
+
export { ProcessIO, type ProcessIOOptions } from './runtime/process-io.js';
|
|
23
|
+
export { HttpIO, type HttpIOOptions } from './runtime/http-io.js';
|
|
24
|
+
export { PyodideIO, type PyodideIOOptions } from './runtime/pyodide-io.js';
|
|
25
|
+
// WorkerPool - concurrent transport management
|
|
26
|
+
export { WorkerPool, type WorkerPoolOptions, type PooledWorker } from './runtime/worker-pool.js';
|
|
27
|
+
// PooledTransport - Transport adapter that wraps WorkerPool
|
|
28
|
+
export { PooledTransport, type PooledTransportOptions } from './runtime/pooled-transport.js';
|
|
29
|
+
export type { Disposable } from './runtime/disposable.js';
|
|
30
|
+
export { isDisposable, safeDispose, disposeAll } from './runtime/disposable.js';
|
|
31
|
+
export {
|
|
32
|
+
ValidationError,
|
|
33
|
+
isFiniteNumber,
|
|
34
|
+
isPositiveNumber,
|
|
35
|
+
isNonNegativeNumber,
|
|
36
|
+
isNonEmptyString,
|
|
37
|
+
isPlainObject,
|
|
38
|
+
assertFiniteNumber,
|
|
39
|
+
assertPositive,
|
|
40
|
+
assertNonNegative,
|
|
41
|
+
assertString,
|
|
42
|
+
assertNonEmptyString,
|
|
43
|
+
assertArray,
|
|
44
|
+
assertObject,
|
|
45
|
+
containsSpecialFloat,
|
|
46
|
+
assertNoSpecialFloats,
|
|
47
|
+
sanitizeForFilename,
|
|
48
|
+
containsPathTraversal,
|
|
49
|
+
} from './runtime/validators.js';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @deprecated Use BoundedContext instead. RuntimeBridge will be removed in the next major version.
|
|
53
|
+
*/
|
|
12
54
|
export { RuntimeBridge } from './runtime/base.js';
|
|
55
|
+
|
|
13
56
|
export {
|
|
14
57
|
BridgeError,
|
|
15
58
|
BridgeProtocolError,
|
|
@@ -19,10 +62,10 @@ export {
|
|
|
19
62
|
} from './runtime/errors.js';
|
|
20
63
|
export { getRuntimeBridge, setRuntimeBridge, clearRuntimeBridge } from './runtime/index.js';
|
|
21
64
|
|
|
22
|
-
// Runtime-specific exports
|
|
23
|
-
export { NodeBridge } from './runtime/node.js';
|
|
24
|
-
export { PyodideBridge } from './runtime/pyodide.js';
|
|
25
|
-
export { HttpBridge } from './runtime/http.js';
|
|
65
|
+
// Runtime-specific exports (Bridges using new BridgeProtocol architecture)
|
|
66
|
+
export { NodeBridge, type NodeBridgeOptions } from './runtime/node.js';
|
|
67
|
+
export { PyodideBridge, type PyodideBridgeOptions } from './runtime/pyodide.js';
|
|
68
|
+
export { HttpBridge, type HttpBridgeOptions } from './runtime/http.js';
|
|
26
69
|
|
|
27
70
|
// Core types
|
|
28
71
|
export type {
|
|
@@ -75,7 +118,7 @@ export {
|
|
|
75
118
|
} from './utils/codec.js';
|
|
76
119
|
|
|
77
120
|
// Version info
|
|
78
|
-
export const VERSION = '0.1
|
|
121
|
+
export const VERSION = '0.2.1';
|
|
79
122
|
|
|
80
123
|
/**
|
|
81
124
|
* Quick setup function for getting started
|
package/src/runtime/base.ts
CHANGED
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Base runtime bridge
|
|
3
|
+
*
|
|
4
|
+
* @deprecated Use BoundedContext instead. RuntimeBridge will be removed in the next major version.
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
|
-
import
|
|
7
|
+
import { BoundedContext } from './bounded-context.js';
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
): Promise<T>;
|
|
21
|
-
|
|
22
|
-
abstract callMethod<T = unknown>(
|
|
23
|
-
handle: string,
|
|
24
|
-
methodName: string,
|
|
25
|
-
args: unknown[],
|
|
26
|
-
kwargs?: Record<string, unknown>
|
|
27
|
-
): Promise<T>;
|
|
28
|
-
|
|
29
|
-
abstract disposeInstance(handle: string): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* @deprecated Use BoundedContext instead. RuntimeBridge will be removed in the next major version.
|
|
11
|
+
*
|
|
12
|
+
* All bridges now extend BoundedContext which provides:
|
|
13
|
+
* - Lifecycle management (init/dispose state machine)
|
|
14
|
+
* - Validation helpers
|
|
15
|
+
* - Error classification
|
|
16
|
+
* - Bounded execution (timeout, retry)
|
|
17
|
+
* - Resource ownership tracking
|
|
18
|
+
*
|
|
19
|
+
* @see BoundedContext
|
|
20
|
+
*/
|
|
21
|
+
export abstract class RuntimeBridge extends BoundedContext {}
|
|
30
22
|
|
|
31
|
-
|
|
32
|
-
}
|
|
23
|
+
// Re-export BoundedContext for backwards compatibility
|
|
24
|
+
export { BoundedContext };
|