mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 00:05:27 +00:00
* fix: connect to dev server instead of localhost * fix: derive WebSocket scheme from location.protocol Mirror the protocol-aware pattern used by initSqlWebSocket in dev.ts so the WebSocket connects over wss:// when the dev server is reached through an HTTPS proxy/tunnel, avoiding mixed-content blocking. * refactor: drop now-unused port parameter of wmillTsDev Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HsfdN82yP88qyQ3h8Lwv2v --------- Co-authored-by: Ruben Fiszel <ruben@windmill.dev> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
161 lines
5.2 KiB
TypeScript
161 lines
5.2 KiB
TypeScript
//comment this line and last to dev
|
|
export function wmillTsDev() { return `
|
|
let reqs: Record<string, any> = {}
|
|
let ws: WebSocket | null = null
|
|
let wsReady: Promise<void>
|
|
let wsReadyResolve: () => void
|
|
|
|
function initWebSocket() {
|
|
wsReady = new Promise((resolve) => {
|
|
wsReadyResolve = resolve
|
|
})
|
|
|
|
ws = new WebSocket((window.location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + window.location.host)
|
|
|
|
ws.onopen = () => {
|
|
console.log('[wmill] WebSocket connected')
|
|
wsReadyResolve()
|
|
}
|
|
|
|
ws.onmessage = (event) => {
|
|
const data = JSON.parse(event.data)
|
|
if (data.type === 'streamJobUpdate') {
|
|
// Handle streaming update
|
|
const job = reqs[data.reqId]
|
|
if (job && job.onUpdate) {
|
|
job.onUpdate({
|
|
new_result_stream: data.new_result_stream,
|
|
stream_offset: data.stream_offset
|
|
})
|
|
}
|
|
} else if (data.type === 'streamJobRes') {
|
|
// Handle stream completion
|
|
const job = reqs[data.reqId]
|
|
if (job) {
|
|
if (data.error) {
|
|
job.reject(new Error(data.result?.stack ?? data.result?.message ?? 'Stream error'))
|
|
} else {
|
|
job.resolve(data.result)
|
|
}
|
|
delete reqs[data.reqId]
|
|
}
|
|
} else if (data.type === 'backendRes' || data.type === 'backendAsyncRes') {
|
|
console.log('Message from WebSocket backend', data)
|
|
const job = reqs[data.reqId]
|
|
if (job) {
|
|
const result = data.result
|
|
if (data.error) {
|
|
job.reject(new Error(result.stack ?? result.message))
|
|
} else {
|
|
job.resolve(result)
|
|
}
|
|
delete reqs[data.reqId]
|
|
} else {
|
|
console.error('No job found for', data.reqId)
|
|
}
|
|
}
|
|
}
|
|
|
|
ws.onerror = (error) => {
|
|
console.error('[wmill] WebSocket error:', error)
|
|
}
|
|
|
|
ws.onclose = () => {
|
|
console.log('[wmill] WebSocket closed, reconnecting...')
|
|
setTimeout(initWebSocket, 1000)
|
|
}
|
|
}
|
|
|
|
initWebSocket()
|
|
|
|
/** A runnable call leaves this page over the WebSocket without touching the DOM,
|
|
* so the session recorder of \`wmill app dev --recording\` (which frames the app)
|
|
* has nothing else to tell it a step is still waiting on the backend. Announcing
|
|
* the request and its answer to the shell mirrors what the deployed runner posts
|
|
* across the same boundary. */
|
|
const framed = typeof window !== 'undefined' && window.parent !== window
|
|
|
|
function notifyRecorder(type: string, reqId: string) {
|
|
if (framed) window.parent.postMessage({ type, reqId }, window.location.origin)
|
|
}
|
|
|
|
// A reload takes the previous context and its WebSocket with it, so whatever it
|
|
// had in flight can never answer. Announcing a fresh module is how the shell
|
|
// learns those calls are dead: a message posted from the unloading document
|
|
// would be dropped with the realm that sent it, and this runs before any app
|
|
// code can issue a call of its own.
|
|
if (framed) {
|
|
window.parent.postMessage({ type: 'wmillDevReady' }, window.location.origin)
|
|
}
|
|
|
|
function tracked(type: string, reqId: string, resolve: (v: any) => void, reject: (e: any) => void) {
|
|
notifyRecorder(type, reqId)
|
|
let settled = false
|
|
const done = () => {
|
|
if (settled) return
|
|
settled = true
|
|
notifyRecorder(type + 'Res', reqId)
|
|
}
|
|
return {
|
|
resolve: (v: any) => { done(); resolve(v) },
|
|
reject: (e: any) => { done(); reject(e) }
|
|
}
|
|
}
|
|
|
|
async function doRequest(type: string, o: object) {
|
|
await wsReady
|
|
return new Promise((resolve, reject) => {
|
|
const reqId = Math.random().toString(36)
|
|
reqs[reqId] = tracked(type, reqId, resolve, reject)
|
|
ws?.send(JSON.stringify({ ...o, type, reqId }))
|
|
})
|
|
}
|
|
|
|
export const backend = new Proxy(
|
|
{},
|
|
{
|
|
get(_, runnable_id: string) {
|
|
return (v: any) => {
|
|
return doRequest('backend', { runnable_id, v })
|
|
}
|
|
}
|
|
})
|
|
|
|
export const backendAsync = new Proxy(
|
|
{},
|
|
{
|
|
get(_, runnable_id: string) {
|
|
return (v: any) => {
|
|
return doRequest('backendAsync', { runnable_id, v })
|
|
}
|
|
}
|
|
})
|
|
|
|
export function waitJob(jobId: string) {
|
|
return doRequest('waitJob', { jobId })
|
|
}
|
|
|
|
export function getJob(jobId: string) {
|
|
return doRequest('getJob', { jobId })
|
|
}
|
|
|
|
/**
|
|
* Stream job results using SSE. Calls onUpdate for each stream update,
|
|
* and resolves with the final result when the job completes.
|
|
* @param jobId - The job ID to stream
|
|
* @param onUpdate - Callback for stream updates with new_result_stream data
|
|
* @returns Promise that resolves with the final job result
|
|
*/
|
|
export function streamJob(
|
|
jobId: string,
|
|
onUpdate?: (data: { new_result_stream?: string; stream_offset?: number }) => void
|
|
): Promise<any> {
|
|
return new Promise(async (resolve, reject) => {
|
|
await wsReady
|
|
const reqId = Math.random().toString(36)
|
|
reqs[reqId] = { ...tracked('streamJob', reqId, resolve, reject), onUpdate }
|
|
ws?.send(JSON.stringify({ jobId, type: 'streamJob', reqId }))
|
|
})
|
|
}
|
|
`}
|