mizan-webview-transport + webview-channels: VSCode webview as Mizan frontend
Two new frontend packages let a VSCode webview consume a Mizan backend
through its postMessage channel — peer transports to `mizan-tauri-transport`
and the default `httpTransport()`.
- `@mizan/webview-transport` implements `MizanTransport` (call/fetch)
over postMessage with correlation-id pairing. Drop-in for `configure({
transport: webviewTransport() })`; codegen output and React adapter
are unchanged.
- `@mizan/webview-channels` mirrors mizan-react's WebSocket-based
ChannelConnection — RPC + subscribe over the same postMessage channel
for long-running ops where short request/reply isn't enough.
Both expect an extension-host-side dispatcher that reads envelopes via
`webview.onDidReceiveMessage` and routes them through mizan-ts's
`handleMutationCall` / `handleContextFetch`. First consumer is the
holomorphic VSCode extension.
mizan-codegen: new `[source.script]` generic source. Spawns an arbitrary
command and reads stdout as KDL IR. Keeps mizan-codegen out of the
business of knowing every possible backend language while preserving
the "subprocess emits KDL" contract every other source already follows.
Holomorphic uses it to invoke `python -m holomorphic.emit_ir` against
the mizan_core registry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
141
frontends/mizan-webview-transport/src/index.ts
Normal file
141
frontends/mizan-webview-transport/src/index.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* @mizan/webview-transport — routes Mizan calls through a VSCode webview's
|
||||
* postMessage channel instead of HTTP fetch or Tauri IPC. Paired with the
|
||||
* extension-host-side Mizan dispatcher (e.g. `MizanHost` in the holomorphic
|
||||
* extension), which receives envelopes via `webview.onDidReceiveMessage`
|
||||
* and posts responses back via `webview.postMessage`.
|
||||
*
|
||||
* Usage (webview side, inside the bundled React/TS app):
|
||||
*
|
||||
* import { configure } from '@mizan/base'
|
||||
* import { webviewTransport } from '@mizan/webview-transport'
|
||||
*
|
||||
* configure({ transport: webviewTransport() })
|
||||
*
|
||||
* The transport keeps the same protocol surface as the HTTP and Tauri
|
||||
* transports (call/fetch envelopes, {result, invalidate, merge} response
|
||||
* shape), so the codegen output and React adapter are unchanged — only
|
||||
* the wire channel differs.
|
||||
*
|
||||
* Envelope shapes (this side ↔ extension-host):
|
||||
*
|
||||
* webview → host:
|
||||
* { kind: 'call', id, fn, args }
|
||||
* { kind: 'fetch', id, context, params? }
|
||||
*
|
||||
* host → webview:
|
||||
* { kind: 'response', id, ok: true, body }
|
||||
* { kind: 'response', id, ok: false, error: { status, body } }
|
||||
*
|
||||
* Correlation by `id` lets multiple in-flight calls share the one
|
||||
* postMessage channel.
|
||||
*/
|
||||
|
||||
import { MizanError, type MizanCallResponse, type MizanTransport } from '@mizan/base'
|
||||
|
||||
// VSCode's webview API — present at runtime, declared globally so the
|
||||
// transport can be authored without pulling vscode types into a generic
|
||||
// frontend package. Returned by acquireVsCodeApi() exactly once per
|
||||
// webview load; subsequent calls throw.
|
||||
declare global {
|
||||
function acquireVsCodeApi(): VsCodeApi
|
||||
}
|
||||
|
||||
interface VsCodeApi {
|
||||
postMessage(msg: unknown): void
|
||||
setState(state: unknown): void
|
||||
getState(): unknown
|
||||
}
|
||||
|
||||
type Pending = {
|
||||
resolve: (data: unknown) => void
|
||||
reject: (err: unknown) => void
|
||||
}
|
||||
|
||||
type CallEnvelope = { kind: 'call'; id: string; fn: string; args: Record<string, any> }
|
||||
type FetchEnvelope = { kind: 'fetch'; id: string; context: string; params?: Record<string, any> }
|
||||
type Envelope = CallEnvelope | FetchEnvelope
|
||||
|
||||
type ResponseOk = { kind: 'response'; id: string; ok: true; body: any }
|
||||
type ResponseErr = { kind: 'response'; id: string; ok: false; error: { status: number; body: string } }
|
||||
type ResponseEnvelope = ResponseOk | ResponseErr
|
||||
|
||||
let _api: VsCodeApi | null = null
|
||||
|
||||
function api(): VsCodeApi {
|
||||
if (_api === null) {
|
||||
if (typeof acquireVsCodeApi !== 'function') {
|
||||
throw new Error(
|
||||
'@mizan/webview-transport: acquireVsCodeApi is not defined. ' +
|
||||
'This transport runs inside a VSCode webview only.',
|
||||
)
|
||||
}
|
||||
_api = acquireVsCodeApi()
|
||||
}
|
||||
return _api
|
||||
}
|
||||
|
||||
const pending = new Map<string, Pending>()
|
||||
let installed = false
|
||||
let counter = 0
|
||||
|
||||
function install(): void {
|
||||
if (installed) return
|
||||
installed = true
|
||||
window.addEventListener('message', (e: MessageEvent) => {
|
||||
const msg = e.data as ResponseEnvelope | undefined
|
||||
if (!msg || msg.kind !== 'response') return
|
||||
const pen = pending.get(msg.id)
|
||||
if (!pen) return
|
||||
pending.delete(msg.id)
|
||||
if (msg.ok) {
|
||||
pen.resolve(msg.body)
|
||||
} else {
|
||||
pen.reject(new MizanError(msg.error.status, msg.error.body))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function nextId(): string {
|
||||
return `mz_${++counter}_${Date.now()}`
|
||||
}
|
||||
|
||||
function send<T>(env: Envelope): Promise<T> {
|
||||
install()
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
pending.set(env.id, {
|
||||
resolve: resolve as (data: unknown) => void,
|
||||
reject,
|
||||
})
|
||||
api().postMessage(env)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Mizan transport that routes through a VSCode webview's
|
||||
* postMessage channel. Install via:
|
||||
*
|
||||
* import { configure } from '@mizan/base'
|
||||
* import { webviewTransport } from '@mizan/webview-transport'
|
||||
* configure({ transport: webviewTransport() })
|
||||
*/
|
||||
export function webviewTransport(): MizanTransport {
|
||||
return {
|
||||
async call(fn, args): Promise<MizanCallResponse> {
|
||||
return send<MizanCallResponse>({
|
||||
kind: 'call',
|
||||
id: nextId(),
|
||||
fn,
|
||||
args,
|
||||
})
|
||||
},
|
||||
async fetch(context, params) {
|
||||
return send<any>({
|
||||
kind: 'fetch',
|
||||
id: nextId(),
|
||||
context,
|
||||
params,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user