-
Notifications
You must be signed in to change notification settings - Fork 0
feat(runtime): add getBridgeInfo() meta call #188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,14 +16,26 @@ | |
| * @see https://github.com/bbopen/tywrap/issues/149 | ||
| */ | ||
|
|
||
| import type { BridgeInfo } from '../types/index.js'; | ||
|
|
||
| import { BoundedContext, type ExecuteOptions } from './bounded-context.js'; | ||
| import { BridgeProtocolError } from './errors.js'; | ||
| import { SafeCodec, type CodecOptions } from './safe-codec.js'; | ||
| import { TYWRAP_PROTOCOL_VERSION } from './protocol.js'; | ||
| import { PROTOCOL_ID, type Transport, type ProtocolMessage } from './transport.js'; | ||
|
|
||
| // ============================================================================= | ||
| // TYPES | ||
| // ============================================================================= | ||
|
|
||
| export interface GetBridgeInfoOptions { | ||
| /** | ||
| * If true, bypasses the cached info and queries the bridge again. | ||
| * This is useful when you want up-to-date instance counts or diagnostics. | ||
| */ | ||
| refresh?: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * Configuration options for BridgeProtocol. | ||
| */ | ||
|
|
@@ -38,6 +50,129 @@ export interface BridgeProtocolOptions { | |
| defaultTimeoutMs?: number; | ||
| } | ||
|
|
||
| function validateBridgeInfoPayload(value: unknown): BridgeInfo { | ||
| if (!value || typeof value !== 'object' || Array.isArray(value)) { | ||
| const kind = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value; | ||
| throw new BridgeProtocolError(`Invalid bridge info payload: expected object, got ${kind}`); | ||
| } | ||
|
|
||
| interface BridgeInfoWire { | ||
| protocol?: unknown; | ||
| protocolVersion?: unknown; | ||
| bridge?: unknown; | ||
| pythonVersion?: unknown; | ||
| pid?: unknown; | ||
| codecFallback?: unknown; | ||
| arrowAvailable?: unknown; | ||
| scipyAvailable?: unknown; | ||
| torchAvailable?: unknown; | ||
| sklearnAvailable?: unknown; | ||
| instances?: unknown; | ||
| } | ||
|
|
||
| const formatValue = (val: unknown): string => { | ||
| try { | ||
| const serialized = JSON.stringify(val); | ||
| return serialized ?? String(val); | ||
| } catch { | ||
| return String(val); | ||
| } | ||
| }; | ||
|
|
||
| const obj = value as BridgeInfoWire; | ||
|
|
||
| const protocol = obj.protocol; | ||
| if (protocol !== PROTOCOL_ID) { | ||
| throw new BridgeProtocolError( | ||
| `Invalid bridge info payload: protocol expected "${PROTOCOL_ID}", got ${formatValue(protocol)}` | ||
| ); | ||
| } | ||
|
|
||
| const protocolVersion = obj.protocolVersion; | ||
| if (protocolVersion !== TYWRAP_PROTOCOL_VERSION) { | ||
| throw new BridgeProtocolError( | ||
| `Invalid bridge info payload: protocolVersion expected ${TYWRAP_PROTOCOL_VERSION}, got ${formatValue(protocolVersion)}` | ||
| ); | ||
| } | ||
|
|
||
| const bridge = obj.bridge; | ||
| if (bridge !== 'python-subprocess') { | ||
| throw new BridgeProtocolError( | ||
| `Invalid bridge info payload: bridge expected "python-subprocess", got ${formatValue(bridge)}` | ||
| ); | ||
| } | ||
|
|
||
| const pythonVersion = obj.pythonVersion; | ||
| if (typeof pythonVersion !== 'string' || pythonVersion.length === 0) { | ||
| throw new BridgeProtocolError( | ||
| `Invalid bridge info payload: pythonVersion expected non-empty string, got ${formatValue(pythonVersion)}` | ||
| ); | ||
| } | ||
|
|
||
| const pid = obj.pid; | ||
| if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0) { | ||
| throw new BridgeProtocolError( | ||
| `Invalid bridge info payload: pid expected positive integer, got ${formatValue(pid)}` | ||
| ); | ||
| } | ||
|
|
||
| const codecFallback = obj.codecFallback; | ||
| if (codecFallback !== 'json' && codecFallback !== 'none') { | ||
| throw new BridgeProtocolError( | ||
| `Invalid bridge info payload: codecFallback expected "json" or "none", got ${formatValue(codecFallback)}` | ||
| ); | ||
| } | ||
|
|
||
| const arrowAvailable = obj.arrowAvailable; | ||
| if (typeof arrowAvailable !== 'boolean') { | ||
| throw new BridgeProtocolError( | ||
| `Invalid bridge info payload: arrowAvailable expected boolean, got ${formatValue(arrowAvailable)}` | ||
| ); | ||
| } | ||
|
|
||
| const scipyAvailable = obj.scipyAvailable; | ||
| if (typeof scipyAvailable !== 'boolean') { | ||
| throw new BridgeProtocolError( | ||
| `Invalid bridge info payload: scipyAvailable expected boolean, got ${formatValue(scipyAvailable)}` | ||
| ); | ||
| } | ||
|
|
||
| const torchAvailable = obj.torchAvailable; | ||
| if (typeof torchAvailable !== 'boolean') { | ||
| throw new BridgeProtocolError( | ||
| `Invalid bridge info payload: torchAvailable expected boolean, got ${formatValue(torchAvailable)}` | ||
| ); | ||
| } | ||
|
|
||
| const sklearnAvailable = obj.sklearnAvailable; | ||
| if (typeof sklearnAvailable !== 'boolean') { | ||
| throw new BridgeProtocolError( | ||
| `Invalid bridge info payload: sklearnAvailable expected boolean, got ${formatValue(sklearnAvailable)}` | ||
| ); | ||
| } | ||
|
|
||
| const instances = obj.instances; | ||
| if (typeof instances !== 'number' || !Number.isInteger(instances) || instances < 0) { | ||
| throw new BridgeProtocolError( | ||
| `Invalid bridge info payload: instances expected non-negative integer, got ${formatValue(instances)}` | ||
| ); | ||
| } | ||
|
Comment on lines
+154
to
+159
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Same concern as 🛡️ Proposed fix const instances = obj.instances;
- if (typeof instances !== 'number' || !Number.isFinite(instances)) {
+ if (typeof instances !== 'number' || !Number.isInteger(instances) || instances < 0) {
throw new BridgeProtocolError(
- `Invalid bridge info payload: instances expected finite number, got ${formatValue(instances)}`
+ `Invalid bridge info payload: instances expected non-negative integer, got ${formatValue(instances)}`
);🤖 Prompt for AI Agents |
||
|
|
||
| return { | ||
| protocol: PROTOCOL_ID, | ||
| protocolVersion: TYWRAP_PROTOCOL_VERSION, | ||
| bridge: 'python-subprocess', | ||
| pythonVersion, | ||
| pid, | ||
| codecFallback, | ||
| arrowAvailable, | ||
| scipyAvailable, | ||
| torchAvailable, | ||
| sklearnAvailable, | ||
| instances, | ||
| }; | ||
| } | ||
|
Comment on lines
+53
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 🧩 Analysis chain🏁 Script executed: # First, find the BridgeInfo type definition
fd src/runtime/bridge-protocol.ts -x head -100 {} | rg -A 20 "type BridgeInfo|interface BridgeInfo"Repository: bbopen/tywrap Length of output: 39 🏁 Script executed: # Search for HttpBridge and PyodideBridge classes
rg -l "class HttpBridge|class PyodideBridge" src/Repository: bbopen/tywrap Length of output: 99 🏁 Script executed: # Check what bridges exist and their getBridgeInfo implementations
rg "getBridgeInfo" -A 5 src/runtime/Repository: bbopen/tywrap Length of output: 824 🏁 Script executed: # Look at the file header and class-level docs mentioned at line 11
head -20 src/runtime/bridge-protocol.tsRepository: bbopen/tywrap Length of output: 811 🏁 Script executed: # Check what HttpBridge returns for bridge identifier
cat -n src/runtime/http.ts | head -150Repository: bbopen/tywrap Length of output: 3227 🏁 Script executed: # Check what PyodideBridge returns
cat -n src/runtime/pyodide.ts | head -150Repository: bbopen/tywrap Length of output: 3289 🏁 Script executed: # Get the full BridgeInfo type definition
rg "type BridgeInfo|interface BridgeInfo" -A 30 src/types/Repository: bbopen/tywrap Length of output: 1337 🏁 Script executed: # Check if there are transport-specific implementations that return different bridge values
rg "bridge.*http|bridge.*pyodide" -i src/runtime/Repository: bbopen/tywrap Length of output: 1462 🏁 Script executed: # Check what each transport returns in its response handling
rg "BridgeInfo|bridge.*:" src/runtime/http-io.ts src/runtime/pyodide-io.ts | head -30Repository: bbopen/tywrap Length of output: 39 🏁 Script executed: # Check if there are tests showing what getBridgeInfo returns for different bridges
fd test src/ | xargs rg "getBridgeInfo|BridgeInfo" | grep -E "http|pyodide" | head -20Repository: bbopen/tywrap Length of output: 39 🏁 Script executed: # Look at the full BridgeInfo type definition to understand the design intent
cat -n src/types/index.ts | rg -A 15 "export interface BridgeInfo"Repository: bbopen/tywrap Length of output: 553 Consider whether The hardcoded Adding field discriminators to error messages would improve debugging: ♻️ Suggested error message improvement- throw new BridgeProtocolError('Invalid bridge info payload');
+ throw new BridgeProtocolError('Invalid bridge info payload: missing or invalid pythonVersion');Similar discriminators for each field would aid debugging on this non-hot path. 🤖 Prompt for AI Agents |
||
|
|
||
| // ============================================================================= | ||
| // BRIDGE PROTOCOL BASE CLASS | ||
| // ============================================================================= | ||
|
|
@@ -80,6 +215,9 @@ export class BridgeProtocol extends BoundedContext { | |
| /** Counter for generating unique request IDs */ | ||
| private requestId = 0; | ||
|
|
||
| /** Cached bridge diagnostics info (populated by getBridgeInfo). */ | ||
| private bridgeInfoCache?: BridgeInfo; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Create a new BridgeProtocol instance. | ||
| * | ||
|
|
@@ -117,6 +255,7 @@ export class BridgeProtocol extends BoundedContext { | |
| * but should not need to dispose the transport manually. | ||
| */ | ||
| protected async doDispose(): Promise<void> { | ||
| this.bridgeInfoCache = undefined; | ||
| // Transport is tracked and will be disposed by BoundedContext | ||
| // Subclasses can override to add additional cleanup | ||
| } | ||
|
|
@@ -316,4 +455,29 @@ export class BridgeProtocol extends BoundedContext { | |
| }, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Fetch bridge diagnostics and feature availability. | ||
| * | ||
| * The Python bridge supports a `meta` method that returns protocol and environment info | ||
| * (including optional codec availability and current instance count). | ||
| */ | ||
| async getBridgeInfo(options: GetBridgeInfoOptions = {}): Promise<BridgeInfo> { | ||
| if (!options.refresh && this.bridgeInfoCache) { | ||
| return this.bridgeInfoCache; | ||
| } | ||
|
|
||
| const info = await this.sendMessage<BridgeInfo>( | ||
| { | ||
| method: 'meta', | ||
| params: {}, | ||
|
Comment on lines
+470
to
+473
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| }, | ||
| { | ||
| validate: validateBridgeInfoPayload, | ||
| } | ||
| ); | ||
|
|
||
| this.bridgeInfoCache = info; | ||
| return info; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.