tywrap 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -32
- package/dist/cli.js +70 -5
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts +11 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +62 -20
- package/dist/config/index.js.map +1 -1
- package/dist/core/analyzer.d.ts +1 -0
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +46 -12
- package/dist/core/analyzer.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +8 -0
- package/dist/core/annotation-parser.d.ts.map +1 -0
- package/dist/core/annotation-parser.js +409 -0
- package/dist/core/annotation-parser.js.map +1 -0
- package/dist/core/discovery.d.ts +1 -0
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +9 -9
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/generator.d.ts +9 -1
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +738 -104
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +9 -7
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +129 -44
- package/dist/core/mapper.js.map +1 -1
- package/dist/dev.d.ts +57 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +743 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/bridge-core.d.ts +1 -0
- package/dist/runtime/bridge-core.d.ts.map +1 -1
- package/dist/runtime/bridge-core.js +52 -17
- package/dist/runtime/bridge-core.js.map +1 -1
- package/dist/runtime/bridge-protocol.d.ts +17 -0
- package/dist/runtime/bridge-protocol.d.ts.map +1 -1
- package/dist/runtime/bridge-protocol.js +97 -0
- package/dist/runtime/bridge-protocol.js.map +1 -1
- package/dist/runtime/errors.d.ts +10 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +9 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/http-io.d.ts.map +1 -1
- package/dist/runtime/http-io.js.map +1 -1
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +6 -0
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +206 -32
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/optimized-node.d.ts +6 -5
- package/dist/runtime/optimized-node.d.ts.map +1 -1
- package/dist/runtime/optimized-node.js +6 -5
- package/dist/runtime/optimized-node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +4 -0
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +2 -0
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/process-io.d.ts +5 -0
- package/dist/runtime/process-io.d.ts.map +1 -1
- package/dist/runtime/process-io.js +64 -30
- package/dist/runtime/process-io.js.map +1 -1
- package/dist/runtime/pyodide-io.d.ts +4 -5
- package/dist/runtime/pyodide-io.d.ts.map +1 -1
- package/dist/runtime/pyodide-io.js +99 -37
- package/dist/runtime/pyodide-io.js.map +1 -1
- package/dist/runtime/safe-codec.d.ts +25 -3
- package/dist/runtime/safe-codec.d.ts.map +1 -1
- package/dist/runtime/safe-codec.js +231 -57
- package/dist/runtime/safe-codec.js.map +1 -1
- package/dist/runtime/transport.d.ts +6 -1
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +0 -11
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +13 -3
- package/dist/runtime/validators.js.map +1 -1
- package/dist/runtime/worker-pool.d.ts +17 -0
- package/dist/runtime/worker-pool.d.ts.map +1 -1
- package/dist/runtime/worker-pool.js +97 -22
- package/dist/runtime/worker-pool.js.map +1 -1
- package/dist/types/index.d.ts +51 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +6 -0
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +237 -331
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +53 -5
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +10 -16
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +28 -0
- package/dist/utils/ir-cache.d.ts.map +1 -0
- package/dist/utils/ir-cache.js +29 -0
- package/dist/utils/ir-cache.js.map +1 -0
- package/dist/utils/parallel-processor.d.ts +1 -1
- package/dist/utils/parallel-processor.d.ts.map +1 -1
- package/dist/utils/parallel-processor.js +111 -40
- package/dist/utils/parallel-processor.js.map +1 -1
- package/dist/utils/runtime.d.ts +5 -1
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +112 -18
- package/dist/utils/runtime.js.map +1 -1
- package/package.json +47 -21
- package/runtime/python_bridge.py +78 -12
- package/src/cli.ts +85 -5
- package/src/config/index.ts +91 -32
- package/src/core/analyzer.ts +51 -15
- package/src/core/annotation-parser.ts +500 -0
- package/src/core/discovery.ts +26 -12
- package/src/core/generator.ts +956 -116
- package/src/core/mapper.ts +158 -51
- package/src/dev.ts +983 -0
- package/src/index.ts +35 -6
- package/src/runtime/bridge-core.ts +58 -18
- package/src/runtime/bridge-protocol.ts +164 -0
- package/src/runtime/errors.ts +14 -0
- package/src/runtime/http-io.ts +2 -1
- package/src/runtime/http.ts +7 -0
- package/src/runtime/node.ts +312 -46
- package/src/runtime/optimized-node.ts +9 -5
- package/src/runtime/pooled-transport.ts +12 -1
- package/src/runtime/process-io.ts +83 -55
- package/src/runtime/pyodide-io.ts +110 -44
- package/src/runtime/safe-codec.ts +290 -82
- package/src/runtime/transport.ts +6 -1
- package/src/runtime/validators.ts +14 -3
- package/src/runtime/worker-pool.ts +120 -24
- package/src/types/global.d.ts +11 -1
- package/src/types/index.ts +63 -8
- package/src/tywrap.ts +296 -361
- package/src/utils/cache.ts +59 -5
- package/src/utils/codec.ts +11 -15
- package/src/utils/ir-cache.ts +51 -0
- package/src/utils/parallel-processor.ts +119 -43
- package/src/utils/runtime.ts +129 -18
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Binary data handling
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { BridgeProtocolError, BridgeExecutionError } from './errors.js';
|
|
11
|
+
import { BridgeCodecError, BridgeProtocolError, BridgeExecutionError } from './errors.js';
|
|
12
12
|
import { containsSpecialFloat } from './validators.js';
|
|
13
13
|
import { decodeValueAsync as decodeArrowValue } from '../utils/codec.js';
|
|
14
14
|
import { PROTOCOL_ID } from './transport.js';
|
|
@@ -42,11 +42,25 @@ interface PythonErrorResponse {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
interface NormalizedPythonError {
|
|
46
|
+
type: string;
|
|
47
|
+
message: string;
|
|
48
|
+
traceback?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ProtocolEnvelope {
|
|
52
|
+
id: number;
|
|
53
|
+
protocol?: string;
|
|
54
|
+
result?: unknown;
|
|
55
|
+
error?: unknown;
|
|
56
|
+
}
|
|
57
|
+
|
|
45
58
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
46
59
|
// CONSTANTS
|
|
47
60
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
48
61
|
|
|
49
62
|
const DEFAULT_MAX_PAYLOAD_BYTES = 10 * 1024 * 1024; // 10MB
|
|
63
|
+
const ERROR_PAYLOAD_SNIPPET_LENGTH = 200;
|
|
50
64
|
|
|
51
65
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
52
66
|
// HELPER FUNCTIONS
|
|
@@ -75,12 +89,20 @@ function buildPath(basePath: string, key: string | number): string {
|
|
|
75
89
|
|
|
76
90
|
/**
|
|
77
91
|
* Recursively check for non-string keys in objects and Maps.
|
|
78
|
-
* Throws
|
|
92
|
+
* Throws BridgeCodecError with path indication if found.
|
|
79
93
|
*/
|
|
80
|
-
function assertStringKeys(
|
|
94
|
+
function assertStringKeys(
|
|
95
|
+
value: unknown,
|
|
96
|
+
path: string = '',
|
|
97
|
+
visited: WeakSet<object> = new WeakSet<object>()
|
|
98
|
+
): void {
|
|
81
99
|
if (value === null || typeof value !== 'object') {
|
|
82
100
|
return;
|
|
83
101
|
}
|
|
102
|
+
if (visited.has(value)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
visited.add(value);
|
|
84
106
|
|
|
85
107
|
// Check Map instances for non-string keys
|
|
86
108
|
if (value instanceof Map) {
|
|
@@ -88,22 +110,23 @@ function assertStringKeys(value: unknown, path: string = ''): void {
|
|
|
88
110
|
if (typeof key !== 'string') {
|
|
89
111
|
const keyDesc = typeof key === 'symbol' ? key.toString() : String(key);
|
|
90
112
|
const location = path ? ` at ${path}` : '';
|
|
91
|
-
throw new
|
|
92
|
-
`Non-string key found in Map${location}: ${keyDesc} (${typeof key})
|
|
113
|
+
throw new BridgeCodecError(
|
|
114
|
+
`Non-string key found in Map${location}: ${keyDesc} (${typeof key})`,
|
|
115
|
+
{ codecPhase: 'encode' }
|
|
93
116
|
);
|
|
94
117
|
}
|
|
95
118
|
}
|
|
96
119
|
// Recurse into Map values
|
|
97
120
|
for (const [key, val] of value.entries()) {
|
|
98
|
-
assertStringKeys(val, buildPath(path, key));
|
|
121
|
+
assertStringKeys(val, buildPath(path, key), visited);
|
|
99
122
|
}
|
|
100
123
|
return;
|
|
101
124
|
}
|
|
102
125
|
|
|
103
126
|
// Check arrays
|
|
104
127
|
if (Array.isArray(value)) {
|
|
105
|
-
for (
|
|
106
|
-
assertStringKeys(
|
|
128
|
+
for (const [index, item] of value.entries()) {
|
|
129
|
+
assertStringKeys(item, buildPath(path, index), visited);
|
|
107
130
|
}
|
|
108
131
|
return;
|
|
109
132
|
}
|
|
@@ -116,14 +139,14 @@ function assertStringKeys(value: unknown, path: string = ''): void {
|
|
|
116
139
|
if (firstSymbol !== undefined) {
|
|
117
140
|
const symbolDesc = firstSymbol.toString();
|
|
118
141
|
const location = path ? ` at ${path}` : '';
|
|
119
|
-
throw new
|
|
120
|
-
|
|
121
|
-
);
|
|
142
|
+
throw new BridgeCodecError(`Symbol key found in object${location}: ${symbolDesc}`, {
|
|
143
|
+
codecPhase: 'encode',
|
|
144
|
+
});
|
|
122
145
|
}
|
|
123
146
|
|
|
124
147
|
// Recurse into object values
|
|
125
|
-
for (const key of Object.
|
|
126
|
-
assertStringKeys(
|
|
148
|
+
for (const [key, item] of Object.entries(value)) {
|
|
149
|
+
assertStringKeys(item, buildPath(path, key), visited);
|
|
127
150
|
}
|
|
128
151
|
}
|
|
129
152
|
}
|
|
@@ -144,22 +167,68 @@ function isPythonErrorResponse(value: unknown): value is PythonErrorResponse {
|
|
|
144
167
|
}
|
|
145
168
|
|
|
146
169
|
/**
|
|
147
|
-
* Type guard for protocol
|
|
148
|
-
*
|
|
149
|
-
* The protocol field is optional as Python may not always include it.
|
|
170
|
+
* Type guard for protocol response envelope.
|
|
171
|
+
* Envelopes must include a numeric id.
|
|
150
172
|
*/
|
|
151
|
-
|
|
152
|
-
id: number;
|
|
153
|
-
protocol?: string;
|
|
154
|
-
result: unknown;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function isProtocolResultResponse(value: unknown): value is ProtocolResultResponse {
|
|
173
|
+
function isProtocolEnvelope(value: unknown): value is ProtocolEnvelope {
|
|
158
174
|
if (value === null || typeof value !== 'object') {
|
|
159
175
|
return false;
|
|
160
176
|
}
|
|
161
177
|
const obj = value as Record<string, unknown>;
|
|
162
|
-
return typeof obj.id === 'number'
|
|
178
|
+
return typeof obj.id === 'number';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function hasOwnKey(value: object, key: string): boolean {
|
|
182
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeErrorPayload(err: unknown): NormalizedPythonError | null {
|
|
186
|
+
if (!err || typeof err !== 'object' || Array.isArray(err)) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const candidate = err as Record<string, unknown>;
|
|
190
|
+
if (typeof candidate.type !== 'string' || typeof candidate.message !== 'string') {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
if (hasOwnKey(candidate, 'traceback') && typeof candidate.traceback !== 'string') {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const normalized: NormalizedPythonError = {
|
|
197
|
+
type: candidate.type,
|
|
198
|
+
message: candidate.message,
|
|
199
|
+
};
|
|
200
|
+
if (typeof candidate.traceback === 'string') {
|
|
201
|
+
normalized.traceback = candidate.traceback;
|
|
202
|
+
}
|
|
203
|
+
return normalized;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function describeInvalidErrorPayload(err: unknown): string {
|
|
207
|
+
if (!err || typeof err !== 'object' || Array.isArray(err)) {
|
|
208
|
+
return 'expected an object with string "type" and "message" fields';
|
|
209
|
+
}
|
|
210
|
+
const candidate = err as Record<string, unknown>;
|
|
211
|
+
if (typeof candidate.type !== 'string') {
|
|
212
|
+
return '"type" must be a string';
|
|
213
|
+
}
|
|
214
|
+
if (typeof candidate.message !== 'string') {
|
|
215
|
+
return '"message" must be a string';
|
|
216
|
+
}
|
|
217
|
+
if (hasOwnKey(candidate, 'traceback') && typeof candidate.traceback !== 'string') {
|
|
218
|
+
return '"traceback" must be a string when provided';
|
|
219
|
+
}
|
|
220
|
+
return 'expected an object with string "type" and "message" fields';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function summarizePayloadForError(payload: string): string {
|
|
224
|
+
const trimmed = payload.trim();
|
|
225
|
+
if (!trimmed) {
|
|
226
|
+
return '[empty payload]';
|
|
227
|
+
}
|
|
228
|
+
if (trimmed.length <= ERROR_PAYLOAD_SNIPPET_LENGTH) {
|
|
229
|
+
return trimmed;
|
|
230
|
+
}
|
|
231
|
+
return `${trimmed.slice(0, ERROR_PAYLOAD_SNIPPET_LENGTH)}...`;
|
|
163
232
|
}
|
|
164
233
|
|
|
165
234
|
/**
|
|
@@ -189,21 +258,32 @@ function validateProtocolVersion(value: unknown): void {
|
|
|
189
258
|
* Find the path to a special float in a value structure.
|
|
190
259
|
* Returns undefined if no special float is found.
|
|
191
260
|
*/
|
|
192
|
-
function findSpecialFloatPath(
|
|
261
|
+
function findSpecialFloatPath(
|
|
262
|
+
value: unknown,
|
|
263
|
+
path: string = '',
|
|
264
|
+
visited: WeakSet<object> = new WeakSet<object>()
|
|
265
|
+
): string | undefined {
|
|
193
266
|
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
194
267
|
return path || 'root';
|
|
195
268
|
}
|
|
269
|
+
if (value === null || typeof value !== 'object') {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
if (visited.has(value)) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
visited.add(value);
|
|
196
276
|
if (Array.isArray(value)) {
|
|
197
|
-
for (
|
|
198
|
-
const result = findSpecialFloatPath(
|
|
277
|
+
for (const [index, item] of value.entries()) {
|
|
278
|
+
const result = findSpecialFloatPath(item, buildPath(path, index), visited);
|
|
199
279
|
if (result !== undefined) {
|
|
200
280
|
return result;
|
|
201
281
|
}
|
|
202
282
|
}
|
|
203
283
|
}
|
|
204
284
|
if (isPlainObject(value)) {
|
|
205
|
-
for (const key of Object.
|
|
206
|
-
const result = findSpecialFloatPath(
|
|
285
|
+
for (const [key, item] of Object.entries(value)) {
|
|
286
|
+
const result = findSpecialFloatPath(item, buildPath(path, key), visited);
|
|
207
287
|
if (result !== undefined) {
|
|
208
288
|
return result;
|
|
209
289
|
}
|
|
@@ -239,12 +319,128 @@ export class SafeCodec {
|
|
|
239
319
|
private readonly rejectNonStringKeys: boolean;
|
|
240
320
|
private readonly maxPayloadBytes: number;
|
|
241
321
|
private readonly bytesHandling: 'base64' | 'reject' | 'passthrough';
|
|
322
|
+
private readonly reviveValueBound: (key: string, value: unknown) => unknown;
|
|
323
|
+
private static readonly base64Pattern =
|
|
324
|
+
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
242
325
|
|
|
243
326
|
constructor(options: CodecOptions = {}) {
|
|
244
327
|
this.rejectSpecialFloats = options.rejectSpecialFloats ?? true;
|
|
245
328
|
this.rejectNonStringKeys = options.rejectNonStringKeys ?? true;
|
|
246
329
|
this.maxPayloadBytes = options.maxPayloadBytes ?? DEFAULT_MAX_PAYLOAD_BYTES;
|
|
247
330
|
this.bytesHandling = options.bytesHandling ?? 'base64';
|
|
331
|
+
this.reviveValueBound = this.reviveValue.bind(this);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private assertValidBase64(b64: string): void {
|
|
335
|
+
if (!SafeCodec.base64Pattern.test(b64)) {
|
|
336
|
+
throw new BridgeCodecError('Invalid base64 in bytes envelope', {
|
|
337
|
+
codecPhase: 'decode',
|
|
338
|
+
valueType: 'bytes',
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Convert base64 string to Uint8Array.
|
|
345
|
+
*
|
|
346
|
+
* Why: Python bridge represents bytes/bytearray as base64 envelopes. Decoding them here
|
|
347
|
+
* restores ergonomic JS types at the boundary.
|
|
348
|
+
*/
|
|
349
|
+
private fromBase64(b64: string): Uint8Array {
|
|
350
|
+
this.assertValidBase64(b64);
|
|
351
|
+
|
|
352
|
+
if (typeof Buffer !== 'undefined') {
|
|
353
|
+
const buf = Buffer.from(b64, 'base64');
|
|
354
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.length);
|
|
355
|
+
}
|
|
356
|
+
if (globalThis.atob) {
|
|
357
|
+
const bin = globalThis.atob(b64);
|
|
358
|
+
const arr = Array.from(bin, c => c.charCodeAt(0));
|
|
359
|
+
return new Uint8Array(arr);
|
|
360
|
+
}
|
|
361
|
+
throw new BridgeCodecError('Base64 decoding is not available in this runtime', {
|
|
362
|
+
codecPhase: 'decode',
|
|
363
|
+
valueType: 'bytes',
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* JSON.parse reviver that decodes bytes envelopes.
|
|
369
|
+
*
|
|
370
|
+
* Supported shapes:
|
|
371
|
+
* - { "__tywrap_bytes__": true, "b64": "..." } (JS SafeCodec.encodeRequest; also allowed in responses)
|
|
372
|
+
* - { "__type__": "bytes", "encoding": "base64", "data": "..." } (Python SafeCodec default encoder)
|
|
373
|
+
*/
|
|
374
|
+
private reviveValue(_key: string, value: unknown): unknown {
|
|
375
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
378
|
+
const obj = value as Record<string, unknown>;
|
|
379
|
+
|
|
380
|
+
if (obj.__tywrap_bytes__ === true && typeof obj.b64 === 'string') {
|
|
381
|
+
try {
|
|
382
|
+
return this.fromBase64(obj.b64);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
385
|
+
throw new BridgeCodecError(`Bytes envelope decode failed: ${errorMessage}`, {
|
|
386
|
+
codecPhase: 'decode',
|
|
387
|
+
valueType: 'bytes',
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (obj.__type__ === 'bytes' && obj.encoding === 'base64' && typeof obj.data === 'string') {
|
|
393
|
+
try {
|
|
394
|
+
return this.fromBase64(obj.data);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
397
|
+
throw new BridgeCodecError(`Bytes envelope decode failed: ${errorMessage}`, {
|
|
398
|
+
codecPhase: 'decode',
|
|
399
|
+
valueType: 'bytes',
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return value;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private toBridgeExecutionError(error: NormalizedPythonError): BridgeExecutionError {
|
|
408
|
+
const bridgeError = new BridgeExecutionError(`${error.type}: ${error.message}`);
|
|
409
|
+
bridgeError.traceback = error.traceback;
|
|
410
|
+
return bridgeError;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private extractResultFromResponseEnvelope(parsed: unknown): unknown {
|
|
414
|
+
if (isProtocolEnvelope(parsed)) {
|
|
415
|
+
const envelope = parsed;
|
|
416
|
+
const hasResult = hasOwnKey(envelope, 'result');
|
|
417
|
+
const hasError = hasOwnKey(envelope, 'error');
|
|
418
|
+
|
|
419
|
+
if (hasResult && hasError) {
|
|
420
|
+
throw new BridgeProtocolError('Protocol response cannot include both "result" and "error"');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (hasError) {
|
|
424
|
+
const normalizedError = normalizeErrorPayload(envelope.error);
|
|
425
|
+
if (!normalizedError) {
|
|
426
|
+
const details = describeInvalidErrorPayload(envelope.error);
|
|
427
|
+
throw new BridgeProtocolError(`Invalid response "error" payload: ${details}`);
|
|
428
|
+
}
|
|
429
|
+
throw this.toBridgeExecutionError(normalizedError);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!hasResult) {
|
|
433
|
+
throw new BridgeProtocolError('Protocol response missing "result" or "error" field');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return envelope.result;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (isPythonErrorResponse(parsed)) {
|
|
440
|
+
throw this.toBridgeExecutionError(parsed.error);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return parsed;
|
|
248
444
|
}
|
|
249
445
|
|
|
250
446
|
/**
|
|
@@ -253,14 +449,15 @@ export class SafeCodec {
|
|
|
253
449
|
*
|
|
254
450
|
* @param message - The message to encode
|
|
255
451
|
* @returns JSON string ready to send
|
|
256
|
-
* @throws
|
|
452
|
+
* @throws BridgeCodecError if validation fails or encoding fails
|
|
257
453
|
*/
|
|
258
454
|
encodeRequest(message: unknown): string {
|
|
259
455
|
// Validate special floats if enabled
|
|
260
456
|
if (this.rejectSpecialFloats && containsSpecialFloat(message)) {
|
|
261
457
|
const floatPath = findSpecialFloatPath(message);
|
|
262
|
-
throw new
|
|
263
|
-
`Cannot encode request: contains non-finite number (NaN or Infinity) at ${floatPath}
|
|
458
|
+
throw new BridgeCodecError(
|
|
459
|
+
`Cannot encode request: contains non-finite number (NaN or Infinity) at ${floatPath}`,
|
|
460
|
+
{ codecPhase: 'encode', valueType: 'number' }
|
|
264
461
|
);
|
|
265
462
|
}
|
|
266
463
|
|
|
@@ -277,8 +474,9 @@ export class SafeCodec {
|
|
|
277
474
|
if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
|
|
278
475
|
switch (this.bytesHandling) {
|
|
279
476
|
case 'reject':
|
|
280
|
-
throw new
|
|
281
|
-
`Cannot encode request: binary data found at ${key || 'root'} (bytesHandling: reject)
|
|
477
|
+
throw new BridgeCodecError(
|
|
478
|
+
`Cannot encode request: binary data found at ${key || 'root'} (bytesHandling: reject)`,
|
|
479
|
+
{ codecPhase: 'encode', valueType: 'bytes' }
|
|
282
480
|
);
|
|
283
481
|
case 'base64': {
|
|
284
482
|
const bytes = value instanceof ArrayBuffer ? new Uint8Array(value) : value;
|
|
@@ -293,18 +491,24 @@ export class SafeCodec {
|
|
|
293
491
|
return value;
|
|
294
492
|
});
|
|
295
493
|
} catch (err) {
|
|
494
|
+
if (err instanceof BridgeCodecError) {
|
|
495
|
+
throw err;
|
|
496
|
+
}
|
|
296
497
|
if (err instanceof BridgeProtocolError) {
|
|
297
498
|
throw err;
|
|
298
499
|
}
|
|
299
|
-
const
|
|
300
|
-
throw new
|
|
500
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
501
|
+
throw new BridgeCodecError(`JSON serialization failed: ${errorMessage}`, {
|
|
502
|
+
codecPhase: 'encode',
|
|
503
|
+
});
|
|
301
504
|
}
|
|
302
505
|
|
|
303
506
|
// Check payload size
|
|
304
507
|
const payloadBytes = new TextEncoder().encode(payload).length;
|
|
305
508
|
if (payloadBytes > this.maxPayloadBytes) {
|
|
306
|
-
throw new
|
|
307
|
-
`Payload size ${payloadBytes} bytes exceeds maximum ${this.maxPayloadBytes} bytes
|
|
509
|
+
throw new BridgeCodecError(
|
|
510
|
+
`Payload size ${payloadBytes} bytes exceeds maximum ${this.maxPayloadBytes} bytes`,
|
|
511
|
+
{ codecPhase: 'encode', valueType: 'payload' }
|
|
308
512
|
);
|
|
309
513
|
}
|
|
310
514
|
|
|
@@ -317,47 +521,46 @@ export class SafeCodec {
|
|
|
317
521
|
*
|
|
318
522
|
* @param payload - The JSON string received from Python
|
|
319
523
|
* @returns Decoded and validated result
|
|
320
|
-
* @throws
|
|
524
|
+
* @throws BridgeCodecError if payload is invalid or decoding fails
|
|
525
|
+
* @throws BridgeProtocolError if protocol envelope is invalid
|
|
321
526
|
* @throws BridgeExecutionError if response contains a Python error
|
|
322
527
|
*/
|
|
323
528
|
decodeResponse<T>(payload: string): T {
|
|
324
529
|
// Check payload size first
|
|
325
530
|
const payloadBytes = new TextEncoder().encode(payload).length;
|
|
326
531
|
if (payloadBytes > this.maxPayloadBytes) {
|
|
327
|
-
throw new
|
|
328
|
-
`Response payload size ${payloadBytes} bytes exceeds maximum ${this.maxPayloadBytes} bytes
|
|
532
|
+
throw new BridgeCodecError(
|
|
533
|
+
`Response payload size ${payloadBytes} bytes exceeds maximum ${this.maxPayloadBytes} bytes`,
|
|
534
|
+
{ codecPhase: 'decode', valueType: 'payload' }
|
|
329
535
|
);
|
|
330
536
|
}
|
|
331
537
|
|
|
332
538
|
// Parse JSON
|
|
333
539
|
let parsed: unknown;
|
|
334
540
|
try {
|
|
335
|
-
parsed = JSON.parse(payload);
|
|
541
|
+
parsed = JSON.parse(payload, this.reviveValueBound);
|
|
336
542
|
} catch (err) {
|
|
337
|
-
|
|
338
|
-
|
|
543
|
+
if (err instanceof BridgeCodecError || err instanceof BridgeProtocolError) {
|
|
544
|
+
throw err;
|
|
545
|
+
}
|
|
546
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
547
|
+
throw new BridgeCodecError(
|
|
548
|
+
`JSON parse failed: ${errorMessage}. Payload snippet: ${summarizePayloadForError(payload)}`,
|
|
549
|
+
{ codecPhase: 'decode', valueType: 'json' }
|
|
550
|
+
);
|
|
339
551
|
}
|
|
340
552
|
|
|
341
553
|
// Validate protocol version (if present)
|
|
342
554
|
validateProtocolVersion(parsed);
|
|
343
555
|
|
|
344
|
-
|
|
345
|
-
if (isPythonErrorResponse(parsed)) {
|
|
346
|
-
const error = new BridgeExecutionError(
|
|
347
|
-
`${parsed.error.type}: ${parsed.error.message}`
|
|
348
|
-
);
|
|
349
|
-
error.traceback = parsed.error.traceback;
|
|
350
|
-
throw error;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Extract the result field from the response envelope
|
|
354
|
-
const result = isProtocolResultResponse(parsed) ? parsed.result : parsed;
|
|
556
|
+
const result = this.extractResultFromResponseEnvelope(parsed);
|
|
355
557
|
|
|
356
558
|
// Post-decode validation for special floats if enabled
|
|
357
559
|
if (this.rejectSpecialFloats && containsSpecialFloat(result)) {
|
|
358
560
|
const floatPath = findSpecialFloatPath(result);
|
|
359
|
-
throw new
|
|
360
|
-
`Response contains non-finite number (NaN or Infinity) at ${floatPath}
|
|
561
|
+
throw new BridgeCodecError(
|
|
562
|
+
`Response contains non-finite number (NaN or Infinity) at ${floatPath}`,
|
|
563
|
+
{ codecPhase: 'decode', valueType: 'number' }
|
|
361
564
|
);
|
|
362
565
|
}
|
|
363
566
|
|
|
@@ -370,57 +573,59 @@ export class SafeCodec {
|
|
|
370
573
|
*
|
|
371
574
|
* @param payload - The JSON string received from Python
|
|
372
575
|
* @returns Decoded and validated result with Arrow decoding applied
|
|
373
|
-
* @throws
|
|
576
|
+
* @throws BridgeCodecError if payload is invalid or decoding fails
|
|
577
|
+
* @throws BridgeProtocolError if protocol envelope is invalid
|
|
374
578
|
* @throws BridgeExecutionError if response contains a Python error
|
|
375
579
|
*/
|
|
376
580
|
async decodeResponseAsync<T>(payload: string): Promise<T> {
|
|
377
581
|
// Check payload size first
|
|
378
582
|
const payloadBytes = new TextEncoder().encode(payload).length;
|
|
379
583
|
if (payloadBytes > this.maxPayloadBytes) {
|
|
380
|
-
throw new
|
|
381
|
-
`Response payload size ${payloadBytes} bytes exceeds maximum ${this.maxPayloadBytes} bytes
|
|
584
|
+
throw new BridgeCodecError(
|
|
585
|
+
`Response payload size ${payloadBytes} bytes exceeds maximum ${this.maxPayloadBytes} bytes`,
|
|
586
|
+
{ codecPhase: 'decode', valueType: 'payload' }
|
|
382
587
|
);
|
|
383
588
|
}
|
|
384
589
|
|
|
385
590
|
// Parse JSON
|
|
386
591
|
let parsed: unknown;
|
|
387
592
|
try {
|
|
388
|
-
parsed = JSON.parse(payload);
|
|
593
|
+
parsed = JSON.parse(payload, this.reviveValueBound);
|
|
389
594
|
} catch (err) {
|
|
390
|
-
|
|
391
|
-
|
|
595
|
+
if (err instanceof BridgeCodecError || err instanceof BridgeProtocolError) {
|
|
596
|
+
throw err;
|
|
597
|
+
}
|
|
598
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
599
|
+
throw new BridgeCodecError(
|
|
600
|
+
`JSON parse failed: ${errorMessage}. Payload snippet: ${summarizePayloadForError(payload)}`,
|
|
601
|
+
{ codecPhase: 'decode', valueType: 'json' }
|
|
602
|
+
);
|
|
392
603
|
}
|
|
393
604
|
|
|
394
605
|
// Validate protocol version (if present)
|
|
395
606
|
validateProtocolVersion(parsed);
|
|
396
607
|
|
|
397
|
-
|
|
398
|
-
if (isPythonErrorResponse(parsed)) {
|
|
399
|
-
const error = new BridgeExecutionError(
|
|
400
|
-
`${parsed.error.type}: ${parsed.error.message}`
|
|
401
|
-
);
|
|
402
|
-
error.traceback = parsed.error.traceback;
|
|
403
|
-
throw error;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Extract the result field from the response envelope
|
|
407
|
-
const result = isProtocolResultResponse(parsed) ? parsed.result : parsed;
|
|
608
|
+
const result = this.extractResultFromResponseEnvelope(parsed);
|
|
408
609
|
|
|
409
610
|
// Apply Arrow decoding to the result
|
|
410
611
|
let decoded: unknown;
|
|
411
612
|
try {
|
|
412
613
|
decoded = await decodeArrowValue(result);
|
|
413
614
|
} catch (err) {
|
|
414
|
-
const
|
|
415
|
-
throw new
|
|
615
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
616
|
+
throw new BridgeCodecError(`Arrow decoding failed: ${errorMessage}`, {
|
|
617
|
+
codecPhase: 'decode',
|
|
618
|
+
valueType: 'arrow',
|
|
619
|
+
});
|
|
416
620
|
}
|
|
417
621
|
|
|
418
622
|
// Post-decode validation for special floats if enabled
|
|
419
|
-
// Note:
|
|
420
|
-
if (this.rejectSpecialFloats && containsSpecialFloat(
|
|
421
|
-
const floatPath = findSpecialFloatPath(
|
|
422
|
-
throw new
|
|
423
|
-
`Response contains non-finite number (NaN or Infinity) at ${floatPath}
|
|
623
|
+
// Note: Arrow decoders can introduce NaN/Infinity from binary representations.
|
|
624
|
+
if (this.rejectSpecialFloats && containsSpecialFloat(decoded)) {
|
|
625
|
+
const floatPath = findSpecialFloatPath(decoded);
|
|
626
|
+
throw new BridgeCodecError(
|
|
627
|
+
`Response contains non-finite number (NaN or Infinity) at ${floatPath}`,
|
|
628
|
+
{ codecPhase: 'decode', valueType: 'number' }
|
|
424
629
|
);
|
|
425
630
|
}
|
|
426
631
|
|
|
@@ -438,6 +643,9 @@ export class SafeCodec {
|
|
|
438
643
|
const binary = String.fromCharCode(...bytes);
|
|
439
644
|
return globalThis.btoa(binary);
|
|
440
645
|
}
|
|
441
|
-
throw new
|
|
646
|
+
throw new BridgeCodecError('Base64 encoding is not available in this runtime', {
|
|
647
|
+
codecPhase: 'encode',
|
|
648
|
+
valueType: 'bytes',
|
|
649
|
+
});
|
|
442
650
|
}
|
|
443
651
|
}
|
package/src/runtime/transport.ts
CHANGED
|
@@ -122,7 +122,12 @@ export interface ProtocolResponse {
|
|
|
122
122
|
* await transport.init();
|
|
123
123
|
*
|
|
124
124
|
* const response = await transport.send(
|
|
125
|
-
* JSON.stringify({
|
|
125
|
+
* JSON.stringify({
|
|
126
|
+
* id: 1,
|
|
127
|
+
* protocol: 'tywrap/1',
|
|
128
|
+
* method: 'call',
|
|
129
|
+
* params: { module: 'math', functionName: 'sqrt', args: [16], kwargs: {} },
|
|
130
|
+
* }),
|
|
126
131
|
* 5000
|
|
127
132
|
* );
|
|
128
133
|
*
|
|
@@ -183,19 +183,30 @@ export function assertObject(value: unknown, name: string): Record<string, unkno
|
|
|
183
183
|
* @param value - The value to check
|
|
184
184
|
* @returns True if the value contains NaN or Infinity anywhere
|
|
185
185
|
*/
|
|
186
|
-
|
|
186
|
+
function containsSpecialFloatRecursive(value: unknown, visited: WeakSet<object>): boolean {
|
|
187
187
|
if (typeof value === 'number') {
|
|
188
188
|
return !Number.isFinite(value);
|
|
189
189
|
}
|
|
190
|
+
if (value === null || typeof value !== 'object') {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (visited.has(value)) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
visited.add(value);
|
|
190
197
|
if (Array.isArray(value)) {
|
|
191
|
-
return value.some(
|
|
198
|
+
return value.some(item => containsSpecialFloatRecursive(item, visited));
|
|
192
199
|
}
|
|
193
200
|
if (isPlainObject(value)) {
|
|
194
|
-
return Object.values(value).some(
|
|
201
|
+
return Object.values(value).some(item => containsSpecialFloatRecursive(item, visited));
|
|
195
202
|
}
|
|
196
203
|
return false;
|
|
197
204
|
}
|
|
198
205
|
|
|
206
|
+
export function containsSpecialFloat(value: unknown): boolean {
|
|
207
|
+
return containsSpecialFloatRecursive(value, new WeakSet<object>());
|
|
208
|
+
}
|
|
209
|
+
|
|
199
210
|
/**
|
|
200
211
|
* Assert that a value does not contain non-finite numbers.
|
|
201
212
|
*
|