tywrap 0.7.0 → 0.9.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 +36 -11
- package/SECURITY.md +39 -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/emit-call.d.ts.map +1 -1
- package/dist/core/emit-call.js +1 -1
- package/dist/core/emit-call.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 +243 -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/dev.d.ts.map +1 -1
- package/dist/dev.js +1 -3
- package/dist/dev.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 +1 -1
- package/dist/runtime/bridge-codec.d.ts.map +1 -1
- package/dist/runtime/bridge-codec.js +78 -55
- 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/frame-codec.d.ts +111 -0
- package/dist/runtime/frame-codec.d.ts.map +1 -0
- package/dist/runtime/frame-codec.js +352 -0
- package/dist/runtime/frame-codec.js.map +1 -0
- 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 +2 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +7 -23
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +5 -78
- 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 +37 -72
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/subprocess-transport.d.ts +134 -8
- package/dist/runtime/subprocess-transport.d.ts.map +1 -1
- package/dist/runtime/subprocess-transport.js +490 -65
- 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 +90 -18
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +21 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +49 -0
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +152 -0
- package/dist/runtime/validators.js.map +1 -1
- package/dist/types/index.d.ts +14 -15
- 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 -21
- 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 +1 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +165 -17
- 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.js +1 -1
- package/package.json +7 -6
- package/runtime/frame_codec.py +430 -0
- package/runtime/python_bridge.py +213 -54
- package/runtime/tywrap_bridge_core.py +217 -147
- package/src/config/index.ts +11 -0
- package/src/core/annotation-parser.ts +9 -5
- package/src/core/emit-call.ts +1 -7
- package/src/core/generator.ts +315 -205
- package/src/core/mapper.ts +8 -8
- package/src/dev.ts +1 -3
- package/src/index.ts +7 -3
- package/src/runtime/base-bridge.ts +5 -30
- package/src/runtime/bounded-context.ts +12 -67
- package/src/runtime/bridge-codec.ts +94 -65
- package/src/runtime/errors.ts +21 -0
- package/src/runtime/frame-codec.ts +469 -0
- package/src/runtime/http-transport.ts +6 -1
- package/src/runtime/index.ts +7 -6
- package/src/runtime/node.ts +17 -104
- 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 +58 -93
- package/src/runtime/subprocess-transport.ts +585 -80
- package/src/runtime/timed-out-request-tracker.ts +1 -1
- package/src/runtime/transport.ts +112 -22
- package/src/runtime/validators.ts +204 -0
- package/src/types/index.ts +21 -35
- package/src/tywrap.ts +157 -30
- package/src/utils/cache.ts +3 -3
- package/src/utils/codec.ts +205 -16
- 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_member_fixtures.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/src/runtime/transport.ts
CHANGED
|
@@ -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
|
// =============================================================================
|
|
@@ -35,9 +57,6 @@ export const TYWRAP_PROTOCOL_VERSION = Number.parseInt(PROTOCOL_ID.split('/')[1]
|
|
|
35
57
|
*
|
|
36
58
|
* Each method corresponds to a BridgeProtocol operation:
|
|
37
59
|
* - `call`: Invoke a module-level function
|
|
38
|
-
* - `instantiate`: Create a new class instance
|
|
39
|
-
* - `call_method`: Invoke a method on an existing instance
|
|
40
|
-
* - `dispose_instance`: Release an instance handle
|
|
41
60
|
* - `meta`: Get bridge metadata
|
|
42
61
|
*/
|
|
43
62
|
export interface ProtocolMessage {
|
|
@@ -48,25 +67,16 @@ export interface ProtocolMessage {
|
|
|
48
67
|
protocol: typeof PROTOCOL_ID;
|
|
49
68
|
|
|
50
69
|
/** The method to invoke */
|
|
51
|
-
method: 'call' | '
|
|
70
|
+
method: 'call' | 'meta';
|
|
52
71
|
|
|
53
72
|
/** Method parameters */
|
|
54
73
|
params: {
|
|
55
|
-
/** Python module path (for call
|
|
74
|
+
/** Python module path (for call) */
|
|
56
75
|
module?: string;
|
|
57
76
|
|
|
58
77
|
/** Function name (for call) */
|
|
59
78
|
functionName?: string;
|
|
60
79
|
|
|
61
|
-
/** Class name (for instantiate) */
|
|
62
|
-
className?: string;
|
|
63
|
-
|
|
64
|
-
/** Instance handle (for call_method and dispose_instance) */
|
|
65
|
-
handle?: string;
|
|
66
|
-
|
|
67
|
-
/** Method name (for call_method) */
|
|
68
|
-
methodName?: string;
|
|
69
|
-
|
|
70
80
|
/** Positional arguments */
|
|
71
81
|
args?: unknown[];
|
|
72
82
|
|
|
@@ -102,6 +112,78 @@ export interface ProtocolResponse {
|
|
|
102
112
|
};
|
|
103
113
|
}
|
|
104
114
|
|
|
115
|
+
// =============================================================================
|
|
116
|
+
// CHUNK FRAMING (tywrap-frame/1)
|
|
117
|
+
// =============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Per-frame encoding for a {@link ChunkFrame}'s `data` field.
|
|
121
|
+
*
|
|
122
|
+
* - `utf8-slice` (the chosen default for 0.8.0): `data` is a raw substring of the
|
|
123
|
+
* complete logical JSON message, split on UTF-8 codepoint boundaries. Because
|
|
124
|
+
* the logical payload is already valid-UTF-8 JSON, the slices reassemble by
|
|
125
|
+
* simple concatenation — no inflation, no extra decode. See
|
|
126
|
+
* docs/transport-framing.md for the rationale (decision #6).
|
|
127
|
+
* - `utf8-base64`: `data` is a base64-encoded chunk of the UTF-8 bytes, safe for
|
|
128
|
+
* arbitrary byte splits but ~33% larger on the wire with a memory-amplification
|
|
129
|
+
* cost. Reserved as an alternative; not emitted by tywrap in 0.8.0.
|
|
130
|
+
*/
|
|
131
|
+
export type ChunkFrameEncoding = 'utf8-base64' | 'utf8-slice';
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A single wire frame of the `tywrap-frame/1` framing protocol.
|
|
135
|
+
*
|
|
136
|
+
* A frame envelope is DISTINCT from the logical {@link ProtocolMessage} /
|
|
137
|
+
* {@link ProtocolResponse}: it carries a slice of the bytes of ONE complete
|
|
138
|
+
* logical JSON message (a request or a response), fragmented because the payload
|
|
139
|
+
* exceeds the transport's frame ceiling. The framing layer reassembles all
|
|
140
|
+
* frames for a given {@link ChunkFrame.id} back into the single logical message
|
|
141
|
+
* before the JSON/codec path ever sees it.
|
|
142
|
+
*
|
|
143
|
+
* Correlation reuses the existing RPC `id`. `seq` is zero-based; `total` and
|
|
144
|
+
* `totalBytes` are repeated on every frame so the receiver can validate the
|
|
145
|
+
* stream is complete (no missing/duplicate `seq`, exact frame count, exact
|
|
146
|
+
* reassembled byte length) before decoding.
|
|
147
|
+
*
|
|
148
|
+
* @see docs/transport-framing.md
|
|
149
|
+
*/
|
|
150
|
+
export interface ChunkFrame {
|
|
151
|
+
/**
|
|
152
|
+
* Frame-envelope discriminator.
|
|
153
|
+
* - `'chunk'`: a normal data-carrying frame.
|
|
154
|
+
* - `'error'`: a framing-layer error (e.g. the sender could not continue the
|
|
155
|
+
* stream); carries no further data frames for this `id`.
|
|
156
|
+
*/
|
|
157
|
+
__tywrap_frame__: 'chunk' | 'error';
|
|
158
|
+
|
|
159
|
+
/** Framing protocol identifier (must equal {@link FRAME_PROTOCOL_ID}). */
|
|
160
|
+
frameProtocol: string;
|
|
161
|
+
|
|
162
|
+
/** Which logical stream this frame belongs to. */
|
|
163
|
+
stream: 'request' | 'response';
|
|
164
|
+
|
|
165
|
+
/** RPC correlation id, shared with the logical {@link ProtocolMessage.id}. */
|
|
166
|
+
id: number;
|
|
167
|
+
|
|
168
|
+
/** Zero-based sequence index of this frame within its stream. */
|
|
169
|
+
seq: number;
|
|
170
|
+
|
|
171
|
+
/** Total number of frames in this stream (repeated on every frame). */
|
|
172
|
+
total: number;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Total byte length of the complete reassembled logical message (repeated on
|
|
176
|
+
* every frame). Used to validate the reassembled payload exactly.
|
|
177
|
+
*/
|
|
178
|
+
totalBytes: number;
|
|
179
|
+
|
|
180
|
+
/** Per-frame payload encoding (see {@link ChunkFrameEncoding}). */
|
|
181
|
+
encoding: ChunkFrameEncoding;
|
|
182
|
+
|
|
183
|
+
/** This frame's slice of the logical message, encoded per {@link encoding}. */
|
|
184
|
+
data: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
105
187
|
// =============================================================================
|
|
106
188
|
// TRANSPORT CAPABILITIES
|
|
107
189
|
// =============================================================================
|
|
@@ -118,9 +200,11 @@ export interface ProtocolResponse {
|
|
|
118
200
|
* authoritative for transport-level flags; the meta report is authoritative for
|
|
119
201
|
* library availability.
|
|
120
202
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
203
|
+
* `supportsChunking` is always true for the subprocess backend because the
|
|
204
|
+
* packaged Python bridge and JS transport always speak `tywrap-frame/1`.
|
|
205
|
+
* HTTP and Pyodide stay `false`. `supportsStreaming` is `false` on every backend
|
|
206
|
+
* (not implemented in 0.8.0). See docs/transport-capabilities.md for the full
|
|
207
|
+
* matrix.
|
|
124
208
|
*/
|
|
125
209
|
export interface TransportCapabilities {
|
|
126
210
|
/** Which backend this transport drives. */
|
|
@@ -142,8 +226,8 @@ export interface TransportCapabilities {
|
|
|
142
226
|
|
|
143
227
|
/**
|
|
144
228
|
* Whether the transport splits a single logical message across multiple wire
|
|
145
|
-
* frames.
|
|
146
|
-
* `false
|
|
229
|
+
* frames. This is always `true` for the packaged subprocess transport and
|
|
230
|
+
* `false` for HTTP and Pyodide.
|
|
147
231
|
*/
|
|
148
232
|
readonly supportsChunking: boolean;
|
|
149
233
|
|
|
@@ -157,7 +241,8 @@ export interface TransportCapabilities {
|
|
|
157
241
|
* Maximum size, in bytes, of a single wire frame the transport will accept.
|
|
158
242
|
* `Number.POSITIVE_INFINITY` means the transport imposes no frame ceiling of
|
|
159
243
|
* its own (a higher layer — e.g. the codec's payload limit — may still cap the
|
|
160
|
-
* size). For the subprocess backend this is the
|
|
244
|
+
* size). For the subprocess backend this is the always-on framing ceiling for
|
|
245
|
+
* each JSONL wire frame.
|
|
161
246
|
*/
|
|
162
247
|
readonly maxFrameBytes: number;
|
|
163
248
|
}
|
|
@@ -228,7 +313,12 @@ export interface Transport extends Disposable {
|
|
|
228
313
|
* @throws BridgeProtocolError if the message format is invalid
|
|
229
314
|
* @throws BridgeError for other transport-level failures
|
|
230
315
|
*/
|
|
231
|
-
send(
|
|
316
|
+
send(
|
|
317
|
+
message: string,
|
|
318
|
+
timeoutMs: number,
|
|
319
|
+
signal?: AbortSignal,
|
|
320
|
+
requestId?: number
|
|
321
|
+
): Promise<string>;
|
|
232
322
|
|
|
233
323
|
/**
|
|
234
324
|
* Whether the transport is ready to send messages.
|
|
@@ -320,7 +410,7 @@ export function isProtocolMessage(value: unknown): value is ProtocolMessage {
|
|
|
320
410
|
typeof msg.id === 'number' &&
|
|
321
411
|
msg.protocol === PROTOCOL_ID &&
|
|
322
412
|
typeof msg.method === 'string' &&
|
|
323
|
-
|
|
413
|
+
(msg.method === 'call' || msg.method === 'meta') &&
|
|
324
414
|
typeof msg.params === 'object' &&
|
|
325
415
|
msg.params !== null
|
|
326
416
|
);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { BridgeValidationError } from './errors.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Pure validation functions for runtime value checking.
|
|
3
5
|
*
|
|
@@ -15,6 +17,208 @@ export class ValidationError extends Error {
|
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
/** A serializable return-contract description emitted by the code generator. */
|
|
21
|
+
export type ReturnSchema =
|
|
22
|
+
| { kind: 'any' }
|
|
23
|
+
| {
|
|
24
|
+
kind: 'primitive';
|
|
25
|
+
type: 'number' | 'string' | 'boolean' | 'null' | 'undefined' | 'Uint8Array' | 'object';
|
|
26
|
+
}
|
|
27
|
+
| { kind: 'literal'; value: string | number | boolean | null }
|
|
28
|
+
| { kind: 'array'; element: ReturnSchema }
|
|
29
|
+
| { kind: 'tuple'; elements: ReturnSchema[] }
|
|
30
|
+
| {
|
|
31
|
+
kind: 'record';
|
|
32
|
+
fields?: Record<string, { schema: ReturnSchema; optional?: boolean }>;
|
|
33
|
+
values?: ReturnSchema;
|
|
34
|
+
}
|
|
35
|
+
| { kind: 'union'; options: ReturnSchema[] }
|
|
36
|
+
| { kind: 'ref'; name: string }
|
|
37
|
+
| { kind: 'marker'; marker: 'dataframe' | 'series' | 'ndarray'; dims?: number; dtype?: string };
|
|
38
|
+
|
|
39
|
+
export type ReturnValidator<T = unknown> = (result: T) => T;
|
|
40
|
+
|
|
41
|
+
export interface DecodedShapeMetadata {
|
|
42
|
+
marker: 'dataframe' | 'series' | 'ndarray';
|
|
43
|
+
dims?: number;
|
|
44
|
+
dtype?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const decodedShapeMetadata = new WeakMap<object, DecodedShapeMetadata>();
|
|
48
|
+
|
|
49
|
+
/** @internal Preserve wire provenance after codec decoding changes the JS shape. */
|
|
50
|
+
export function tagDecodedShape<T>(value: T, metadata: DecodedShapeMetadata): T {
|
|
51
|
+
if (value !== null && (typeof value === 'object' || typeof value === 'function')) {
|
|
52
|
+
decodedShapeMetadata.set(value as object, metadata);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isObjectLike(value: unknown): value is object {
|
|
58
|
+
return value !== null && (typeof value === 'object' || typeof value === 'function');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A short, bounded description suitable for an error message. */
|
|
62
|
+
export function describeReceivedShape(value: unknown): string {
|
|
63
|
+
if (value === null) {
|
|
64
|
+
return 'null';
|
|
65
|
+
}
|
|
66
|
+
if (value === undefined) {
|
|
67
|
+
return 'undefined';
|
|
68
|
+
}
|
|
69
|
+
if (value instanceof Uint8Array) {
|
|
70
|
+
return `Uint8Array(${value.byteLength})`;
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
return `array(${value.length})`;
|
|
74
|
+
}
|
|
75
|
+
if (typeof value === 'object') {
|
|
76
|
+
const marker = decodedShapeMetadata.get(value);
|
|
77
|
+
if (marker) {
|
|
78
|
+
const details = [marker.dims === undefined ? undefined : `${marker.dims}d`, marker.dtype]
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join(', ');
|
|
81
|
+
return `${marker.marker}${details ? ` (${details})` : ''}`;
|
|
82
|
+
}
|
|
83
|
+
const name = (value as { constructor?: { name?: unknown } }).constructor?.name;
|
|
84
|
+
return typeof name === 'string' && name !== 'Object' ? name : 'object';
|
|
85
|
+
}
|
|
86
|
+
return typeof value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderSchema(schema: ReturnSchema): string {
|
|
90
|
+
switch (schema.kind) {
|
|
91
|
+
case 'any':
|
|
92
|
+
return 'unknown';
|
|
93
|
+
case 'primitive':
|
|
94
|
+
return schema.type;
|
|
95
|
+
case 'literal':
|
|
96
|
+
return JSON.stringify(schema.value);
|
|
97
|
+
case 'array':
|
|
98
|
+
return `${renderSchema(schema.element)}[]`;
|
|
99
|
+
case 'tuple':
|
|
100
|
+
return `[${schema.elements.map(renderSchema).join(', ')}]`;
|
|
101
|
+
case 'record':
|
|
102
|
+
return 'record';
|
|
103
|
+
case 'union':
|
|
104
|
+
return schema.options.map(renderSchema).join(' | ');
|
|
105
|
+
case 'ref':
|
|
106
|
+
return schema.name;
|
|
107
|
+
case 'marker':
|
|
108
|
+
return schema.marker;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface CheckState {
|
|
113
|
+
readonly definitions: Readonly<Record<string, ReturnSchema>>;
|
|
114
|
+
readonly pairs: WeakMap<object, Set<string>>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function check(schema: ReturnSchema, value: unknown, state: CheckState): boolean {
|
|
118
|
+
if (schema.kind === 'any') {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (schema.kind === 'ref') {
|
|
123
|
+
const definition = state.definitions[schema.name];
|
|
124
|
+
if (!definition) {
|
|
125
|
+
return true;
|
|
126
|
+
} // unresolved/erased types deliberately degrade to unknown.
|
|
127
|
+
if (isObjectLike(value)) {
|
|
128
|
+
const seen = state.pairs.get(value) ?? new Set<string>();
|
|
129
|
+
if (seen.has(schema.name)) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
seen.add(schema.name);
|
|
133
|
+
state.pairs.set(value, seen);
|
|
134
|
+
}
|
|
135
|
+
return check(definition, value, state);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
switch (schema.kind) {
|
|
139
|
+
case 'primitive':
|
|
140
|
+
if (schema.type === 'null') {
|
|
141
|
+
return value === null;
|
|
142
|
+
}
|
|
143
|
+
if (schema.type === 'undefined') {
|
|
144
|
+
return value === undefined;
|
|
145
|
+
}
|
|
146
|
+
if (schema.type === 'Uint8Array') {
|
|
147
|
+
return value instanceof Uint8Array;
|
|
148
|
+
}
|
|
149
|
+
if (schema.type === 'object') {
|
|
150
|
+
return isPlainObject(value);
|
|
151
|
+
}
|
|
152
|
+
return typeof value === schema.type;
|
|
153
|
+
case 'literal':
|
|
154
|
+
return Object.is(value, schema.value);
|
|
155
|
+
case 'array':
|
|
156
|
+
return Array.isArray(value) && value.every(item => check(schema.element, item, state));
|
|
157
|
+
case 'tuple':
|
|
158
|
+
return (
|
|
159
|
+
Array.isArray(value) &&
|
|
160
|
+
value.length === schema.elements.length &&
|
|
161
|
+
schema.elements.every((entry, index) => check(entry, value[index], state))
|
|
162
|
+
);
|
|
163
|
+
case 'record': {
|
|
164
|
+
if (!isPlainObject(value)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (schema.fields) {
|
|
168
|
+
for (const [key, field] of Object.entries(schema.fields)) {
|
|
169
|
+
if (!(key in value)) {
|
|
170
|
+
if (field.optional) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (!check(field.schema, value[key], state)) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return (
|
|
181
|
+
!schema.values ||
|
|
182
|
+
Object.values(value).every(item => check(schema.values as ReturnSchema, item, state))
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
case 'union':
|
|
186
|
+
return schema.options.some(option => check(option, value, state));
|
|
187
|
+
case 'marker': {
|
|
188
|
+
const metadata = isObjectLike(value) ? decodedShapeMetadata.get(value) : undefined;
|
|
189
|
+
if (metadata?.marker !== schema.marker) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return (
|
|
193
|
+
(schema.dims === undefined || metadata.dims === schema.dims) &&
|
|
194
|
+
(schema.dtype === undefined || metadata.dtype === schema.dtype)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Construct a cycle-safe structural validator for one generated callable.
|
|
202
|
+
* Unknown/Any/void schemas intentionally validate nothing.
|
|
203
|
+
*/
|
|
204
|
+
export function createReturnValidator<T = unknown>(
|
|
205
|
+
schema: ReturnSchema,
|
|
206
|
+
callSite: string,
|
|
207
|
+
definitions: Readonly<Record<string, ReturnSchema>> = {}
|
|
208
|
+
): ReturnValidator<T> {
|
|
209
|
+
const declaredType = renderSchema(schema);
|
|
210
|
+
return (result: T): T => {
|
|
211
|
+
if (!check(schema, result, { definitions, pairs: new WeakMap<object, Set<string>>() })) {
|
|
212
|
+
throw new BridgeValidationError({
|
|
213
|
+
declaredType,
|
|
214
|
+
receivedShape: describeReceivedShape(result),
|
|
215
|
+
callSite,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
18
222
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
223
|
// TYPE GUARDS
|
|
20
224
|
// ═══════════════════════════════════════════════════════════════════════════
|
package/src/types/index.ts
CHANGED
|
@@ -13,6 +13,13 @@ export interface PythonModule {
|
|
|
13
13
|
exports: string[];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** A pinned, versioned JSON IR artifact emitted alongside generated wrappers. */
|
|
17
|
+
export interface IrContract {
|
|
18
|
+
ir_version: string;
|
|
19
|
+
module: string;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
/**
|
|
17
24
|
* How a callable is bound on its owning class.
|
|
18
25
|
*
|
|
@@ -261,7 +268,8 @@ export interface TSPrimitiveType {
|
|
|
261
268
|
| 'void'
|
|
262
269
|
| 'unknown'
|
|
263
270
|
| 'never'
|
|
264
|
-
| 'object'
|
|
271
|
+
| 'object'
|
|
272
|
+
| 'Uint8Array';
|
|
265
273
|
}
|
|
266
274
|
|
|
267
275
|
export interface TSArrayType {
|
|
@@ -314,6 +322,7 @@ export interface TSParameter {
|
|
|
314
322
|
export interface TSGenericType {
|
|
315
323
|
kind: 'generic';
|
|
316
324
|
name: string;
|
|
325
|
+
module?: string;
|
|
317
326
|
typeArgs: TypescriptType[];
|
|
318
327
|
}
|
|
319
328
|
|
|
@@ -338,6 +347,11 @@ export interface TywrapOptions {
|
|
|
338
347
|
* and discovery (IR extraction). Useful for local modules not installed in site-packages.
|
|
339
348
|
*/
|
|
340
349
|
pythonImportPath?: string[];
|
|
350
|
+
/**
|
|
351
|
+
* Read one pinned IR contract instead of spawning Python. A string applies to
|
|
352
|
+
* every configured module; a record selects a contract path per module.
|
|
353
|
+
*/
|
|
354
|
+
contractInput?: string | Record<string, string>;
|
|
341
355
|
output: OutputConfig;
|
|
342
356
|
runtime: RuntimeConfig;
|
|
343
357
|
performance: PerformanceConfig;
|
|
@@ -404,6 +418,8 @@ export interface PerformanceConfig {
|
|
|
404
418
|
compression: 'auto' | 'gzip' | 'brotli' | 'none';
|
|
405
419
|
}
|
|
406
420
|
|
|
421
|
+
// `numpy` is intentionally accepted as a no-op in 0.9: decoded ndarrays are JS
|
|
422
|
+
// arrays, and a faithful static shape awaits #268 return validation.
|
|
407
423
|
export type TypePreset = 'numpy' | 'pandas' | 'pydantic' | 'stdlib' | 'scipy' | 'torch' | 'sklearn';
|
|
408
424
|
|
|
409
425
|
export interface TypeMappingConfig {
|
|
@@ -474,7 +490,7 @@ export interface GenerationMetadata {
|
|
|
474
490
|
optimizations: string[];
|
|
475
491
|
}
|
|
476
492
|
|
|
477
|
-
// PythonRuntime — the
|
|
493
|
+
// PythonRuntime — the call-only cross-boundary RPC method generated wrappers call.
|
|
478
494
|
// This is the contract that, after the composition rework, is implemented ONLY
|
|
479
495
|
// by the bridge facades (NodeBridge/HttpBridge/PyodideBridge); the facades
|
|
480
496
|
// satisfy it by delegating to an owned RpcClient. It deliberately carries NO
|
|
@@ -486,45 +502,15 @@ export interface PythonRuntime {
|
|
|
486
502
|
module: string,
|
|
487
503
|
functionName: string,
|
|
488
504
|
args: unknown[],
|
|
489
|
-
kwargs?: Record<string, unknown
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
instantiate<T = unknown>(
|
|
493
|
-
module: string,
|
|
494
|
-
className: string,
|
|
495
|
-
args: unknown[],
|
|
496
|
-
kwargs?: Record<string, unknown>
|
|
505
|
+
kwargs?: Record<string, unknown>,
|
|
506
|
+
validate?: (result: T) => void
|
|
497
507
|
): Promise<T>;
|
|
498
|
-
|
|
499
|
-
callMethod<T = unknown>(
|
|
500
|
-
handle: string,
|
|
501
|
-
methodName: string,
|
|
502
|
-
args: unknown[],
|
|
503
|
-
kwargs?: Record<string, unknown>
|
|
504
|
-
): Promise<T>;
|
|
505
|
-
|
|
506
|
-
disposeInstance(handle: string): Promise<void>;
|
|
507
508
|
}
|
|
508
509
|
|
|
509
|
-
// Runtime bridge interface — the
|
|
510
|
+
// Runtime bridge interface — the call RPC method plus lifecycle dispose().
|
|
510
511
|
// Kept as PythonRuntime + dispose() so getRuntimeBridge() and every existing
|
|
511
512
|
// `RuntimeExecution` reference (registry, dev.ts) compile with zero churn. The
|
|
512
513
|
// RuntimeExecution -> PythonRuntime symbol rename is deferred to the T9 pass.
|
|
513
514
|
export interface RuntimeExecution extends PythonRuntime {
|
|
514
515
|
dispose(): Promise<void>;
|
|
515
516
|
}
|
|
516
|
-
|
|
517
|
-
// Utility types
|
|
518
|
-
export type DeepReadonly<T> = {
|
|
519
|
-
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
523
|
-
|
|
524
|
-
export type RequiredKeys<T> = {
|
|
525
|
-
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
|
|
526
|
-
}[keyof T];
|
|
527
|
-
|
|
528
|
-
export type OptionalKeys<T> = {
|
|
529
|
-
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
|
|
530
|
-
}[keyof T];
|