2023-06-22 16:43:33 +10:00
|
|
|
import { SourceRange } from '../executor'
|
|
|
|
import { Selections } from '../../useStore'
|
2023-08-01 09:36:40 -05:00
|
|
|
import { VITE_KC_API_WS_MODELING_URL } from '../../env'
|
2023-08-02 15:41:59 +10:00
|
|
|
import { Models } from '@kittycad/lib'
|
2023-08-02 16:23:17 -07:00
|
|
|
import { exportSave } from '../../lib/exportSave'
|
2023-08-09 20:49:10 +10:00
|
|
|
import { v4 as uuidv4 } from 'uuid'
|
2023-06-22 16:43:33 +10:00
|
|
|
|
|
|
|
interface ResultCommand {
|
|
|
|
type: 'result'
|
|
|
|
data: any
|
|
|
|
}
|
|
|
|
interface PendingCommand {
|
|
|
|
type: 'pending'
|
|
|
|
promise: Promise<any>
|
|
|
|
resolve: (val: any) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ArtifactMap {
|
|
|
|
[key: string]: ResultCommand | PendingCommand
|
|
|
|
}
|
|
|
|
export interface SourceRangeMap {
|
|
|
|
[key: string]: SourceRange
|
|
|
|
}
|
|
|
|
|
|
|
|
interface SelectionsArgs {
|
|
|
|
id: string
|
|
|
|
type: Selections['codeBasedSelections'][number]['type']
|
|
|
|
}
|
|
|
|
|
|
|
|
interface CursorSelectionsArgs {
|
|
|
|
otherSelections: Selections['otherSelections']
|
|
|
|
idBasedSelections: { type: string; id: string }[]
|
|
|
|
}
|
|
|
|
|
2023-08-18 08:12:32 -07:00
|
|
|
export type EngineCommand = Models['WebSocketMessages_type']
|
2023-08-02 15:41:59 +10:00
|
|
|
|
2023-08-18 08:12:32 -07:00
|
|
|
type OkResponse = Models['OkModelingCmdResponse_type']
|
2023-06-22 16:43:33 +10:00
|
|
|
|
2023-08-18 08:12:32 -07:00
|
|
|
type WebSocketResponse = Models['WebSocketResponses_type']
|
2023-08-09 20:49:10 +10:00
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
// EngineConnection encapsulates the connection(s) to the Engine
|
|
|
|
// for the EngineCommandManager; namely, the underlying WebSocket
|
|
|
|
// and WebRTC connections.
|
|
|
|
export class EngineConnection {
|
|
|
|
websocket?: WebSocket
|
2023-06-22 16:43:33 +10:00
|
|
|
pc?: RTCPeerConnection
|
2023-06-23 09:56:37 +10:00
|
|
|
lossyDataChannel?: RTCDataChannel
|
2023-08-18 16:16:16 -04:00
|
|
|
|
|
|
|
onConnectionStarted: (conn: EngineConnection) => void = () => {}
|
|
|
|
|
2023-07-10 15:15:07 +10:00
|
|
|
waitForReady: Promise<void> = new Promise(() => {})
|
|
|
|
private resolveReady = () => {}
|
2023-08-18 16:16:16 -04:00
|
|
|
|
|
|
|
readonly url: string
|
|
|
|
private readonly token?: string
|
|
|
|
|
2023-06-23 14:19:15 +10:00
|
|
|
constructor({
|
2023-08-18 16:16:16 -04:00
|
|
|
url,
|
2023-07-11 20:34:09 +10:00
|
|
|
token,
|
2023-08-18 16:16:16 -04:00
|
|
|
onConnectionStarted,
|
2023-06-23 14:19:15 +10:00
|
|
|
}: {
|
2023-08-18 16:16:16 -04:00
|
|
|
url: string
|
2023-07-11 20:34:09 +10:00
|
|
|
token?: string
|
2023-08-18 16:16:16 -04:00
|
|
|
onConnectionStarted: (conn: EngineConnection) => void
|
2023-06-23 14:19:15 +10:00
|
|
|
}) {
|
2023-08-18 16:16:16 -04:00
|
|
|
this.url = url
|
|
|
|
this.token = token
|
|
|
|
this.onConnectionStarted = onConnectionStarted
|
|
|
|
|
|
|
|
// TODO(paultag): This isn't right; this should be when the
|
|
|
|
// connection is in a good place, and tied to the connect() method,
|
|
|
|
// but this is part of a larger refactor to untangle logic. Once the
|
|
|
|
// Connection is pulled apart, we can rework how ready is represented.
|
|
|
|
// This was just the easiest way to ensure some level of parity between
|
|
|
|
// the CommandManager and the Connection until I send a rework for
|
|
|
|
// retry logic.
|
2023-07-10 15:15:07 +10:00
|
|
|
this.waitForReady = new Promise((resolve) => {
|
|
|
|
this.resolveReady = resolve
|
|
|
|
})
|
2023-08-18 16:16:16 -04:00
|
|
|
}
|
2023-08-02 16:23:17 -07:00
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
connect() {
|
|
|
|
this.websocket = new WebSocket(this.url, [])
|
|
|
|
|
|
|
|
this.websocket.binaryType = 'arraybuffer'
|
2023-08-02 16:23:17 -07:00
|
|
|
|
2023-06-22 16:43:33 +10:00
|
|
|
this.pc = new RTCPeerConnection()
|
2023-06-23 10:54:19 +10:00
|
|
|
this.pc.createDataChannel('unreliable_modeling_cmds')
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket.addEventListener('open', (event) => {
|
2023-06-22 16:43:33 +10:00
|
|
|
console.log('Connected to websocket, waiting for ICE servers')
|
2023-08-18 16:16:16 -04:00
|
|
|
if (this.token) {
|
|
|
|
this.websocket?.send(
|
|
|
|
JSON.stringify({ headers: { Authorization: `Bearer ${this.token}` } })
|
2023-07-11 20:34:09 +10:00
|
|
|
)
|
|
|
|
}
|
2023-06-22 16:43:33 +10:00
|
|
|
})
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket.addEventListener('close', (event) => {
|
2023-08-02 13:51:05 -05:00
|
|
|
console.log('websocket connection closed', event)
|
2023-06-22 16:43:33 +10:00
|
|
|
})
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket.addEventListener('error', (event) => {
|
2023-08-02 13:51:05 -05:00
|
|
|
console.log('websocket connection error', event)
|
2023-06-22 16:43:33 +10:00
|
|
|
})
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket.addEventListener('message', (event) => {
|
|
|
|
// In the EngineConnection, we're looking for messages to/from
|
|
|
|
// the server that relate to the ICE handshake, or WebRTC
|
|
|
|
// negotiation. There may be other messages (including ArrayBuffer
|
|
|
|
// messages) that are intended for the GUI itself, so be careful
|
|
|
|
// when assuming we're the only consumer or that all messages will
|
|
|
|
// be carefully formatted here.
|
|
|
|
|
|
|
|
if (typeof event.data !== 'string') {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const message: WebSocketResponse = JSON.parse(event.data)
|
|
|
|
|
|
|
|
if (
|
|
|
|
message.type === 'sdp_answer' &&
|
|
|
|
message.answer.type !== 'unspecified'
|
2023-06-22 16:43:33 +10:00
|
|
|
) {
|
2023-08-18 16:16:16 -04:00
|
|
|
this.pc?.setRemoteDescription(
|
|
|
|
new RTCSessionDescription({
|
|
|
|
type: message.answer.type,
|
|
|
|
sdp: message.answer.sdp,
|
2023-06-22 16:43:33 +10:00
|
|
|
})
|
2023-08-18 16:16:16 -04:00
|
|
|
)
|
|
|
|
} else if (message.type === 'trickle_ice') {
|
|
|
|
this.pc?.addIceCandidate(message.candidate as RTCIceCandidateInit)
|
|
|
|
} else if (message.type === 'ice_server_info' && this.pc) {
|
|
|
|
console.log('received ice_server_info')
|
2023-06-22 16:43:33 +10:00
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
if (message.ice_servers.length > 0) {
|
|
|
|
// When we set the Configuration, we want to always force
|
|
|
|
// iceTransportPolicy to 'relay', since we know the topology
|
|
|
|
// of the ICE/STUN/TUN server and the engine. We don't wish to
|
|
|
|
// talk to the engine in any configuration /other/ than relay
|
|
|
|
// from a infra POV.
|
|
|
|
this.pc.setConfiguration({
|
|
|
|
iceServers: message.ice_servers,
|
|
|
|
iceTransportPolicy: 'relay',
|
2023-06-22 16:43:33 +10:00
|
|
|
})
|
2023-08-18 16:16:16 -04:00
|
|
|
} else {
|
|
|
|
this.pc?.setConfiguration({})
|
|
|
|
}
|
2023-06-22 16:43:33 +10:00
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
// We have an ICE Servers set now. We just setConfiguration, so let's
|
|
|
|
// start adding things we care about to the PeerConnection and let
|
|
|
|
// ICE negotiation happen in the background. Everything from here
|
|
|
|
// until the end of this function is setup of our end of the
|
|
|
|
// PeerConnection and waiting for events to fire our callbacks.
|
|
|
|
|
|
|
|
this.pc.addEventListener('connectionstatechange', (e) =>
|
|
|
|
console.log(this.pc?.iceConnectionState)
|
|
|
|
)
|
|
|
|
|
|
|
|
this.pc.addEventListener('icecandidate', (event) => {
|
|
|
|
if (!this.pc || !this.websocket) return
|
|
|
|
if (event.candidate === null) {
|
|
|
|
console.log('sent sdp_offer')
|
|
|
|
this.websocket.send(
|
|
|
|
JSON.stringify({
|
2023-08-03 05:51:52 +10:00
|
|
|
type: 'sdp_offer',
|
2023-08-18 16:16:16 -04:00
|
|
|
offer: this.pc.localDescription,
|
2023-06-22 16:43:33 +10:00
|
|
|
})
|
2023-08-18 16:16:16 -04:00
|
|
|
)
|
|
|
|
} else {
|
|
|
|
console.log('sending trickle ice candidate')
|
|
|
|
const { candidate } = event
|
|
|
|
this.websocket?.send(
|
|
|
|
JSON.stringify({
|
|
|
|
type: 'trickle_ice',
|
|
|
|
candidate: candidate.toJSON(),
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// Offer to receive 1 video track
|
|
|
|
this.pc.addTransceiver('video', {})
|
|
|
|
|
|
|
|
// Finally (but actually firstly!), to kick things off, we're going to
|
|
|
|
// generate our SDP, set it on our PeerConnection, and let the server
|
|
|
|
// know about our capabilities.
|
|
|
|
this.pc
|
|
|
|
.createOffer()
|
|
|
|
.then(async (descriptionInit) => {
|
|
|
|
await this?.pc?.setLocalDescription(descriptionInit)
|
|
|
|
console.log('sent sdp_offer begin')
|
|
|
|
const msg = JSON.stringify({
|
|
|
|
type: 'sdp_offer',
|
|
|
|
offer: this.pc?.localDescription,
|
2023-06-23 09:56:37 +10:00
|
|
|
})
|
2023-08-18 16:16:16 -04:00
|
|
|
this.websocket?.send(msg)
|
2023-06-23 09:56:37 +10:00
|
|
|
})
|
2023-08-18 16:16:16 -04:00
|
|
|
.catch(console.log)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
this.pc.addEventListener('datachannel', (event) => {
|
|
|
|
this.lossyDataChannel = event.channel
|
|
|
|
|
|
|
|
console.log('accepted lossy data channel', event.channel.label)
|
|
|
|
this.lossyDataChannel.addEventListener('open', (event) => {
|
|
|
|
this.resolveReady()
|
|
|
|
console.log('lossy data channel opened', event)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.lossyDataChannel.addEventListener('close', (event) => {
|
|
|
|
console.log('lossy data channel closed')
|
|
|
|
})
|
|
|
|
|
|
|
|
this.lossyDataChannel.addEventListener('error', (event) => {
|
|
|
|
console.log('lossy data channel error')
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
if (this.onConnectionStarted) this.onConnectionStarted(this)
|
|
|
|
}
|
|
|
|
close() {
|
|
|
|
this.websocket?.close()
|
|
|
|
this.pc?.close()
|
|
|
|
this.lossyDataChannel?.close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class EngineCommandManager {
|
|
|
|
artifactMap: ArtifactMap = {}
|
|
|
|
sourceRangeMap: SourceRangeMap = {}
|
|
|
|
outSequence = 1
|
|
|
|
inSequence = 1
|
|
|
|
engineConnection?: EngineConnection
|
|
|
|
waitForReady: Promise<void> = new Promise(() => {})
|
|
|
|
private resolveReady = () => {}
|
|
|
|
onHoverCallback: (id?: string) => void = () => {}
|
|
|
|
onClickCallback: (selection?: SelectionsArgs) => void = () => {}
|
|
|
|
onCursorsSelectedCallback: (selections: CursorSelectionsArgs) => void =
|
|
|
|
() => {}
|
|
|
|
constructor({
|
|
|
|
setMediaStream,
|
|
|
|
setIsStreamReady,
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
token,
|
|
|
|
}: {
|
|
|
|
setMediaStream: (stream: MediaStream) => void
|
|
|
|
setIsStreamReady: (isStreamReady: boolean) => void
|
|
|
|
width: number
|
|
|
|
height: number
|
|
|
|
token?: string
|
|
|
|
}) {
|
|
|
|
this.waitForReady = new Promise((resolve) => {
|
|
|
|
this.resolveReady = resolve
|
|
|
|
})
|
|
|
|
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}`
|
|
|
|
this.engineConnection = new EngineConnection({
|
|
|
|
url,
|
|
|
|
token,
|
|
|
|
onConnectionStarted: (conn) => {
|
|
|
|
this.engineConnection?.pc?.addEventListener('track', (event) => {
|
|
|
|
console.log('received track', event)
|
|
|
|
const mediaStream = event.streams[0]
|
|
|
|
setMediaStream(mediaStream)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.engineConnection?.pc?.addEventListener('datachannel', (event) => {
|
|
|
|
let lossyDataChannel = event.channel
|
|
|
|
lossyDataChannel.addEventListener('message', (event) => {
|
|
|
|
const result: OkResponse = JSON.parse(event.data)
|
|
|
|
if (
|
|
|
|
result.type === 'highlight_set_entity' &&
|
|
|
|
result.sequence &&
|
|
|
|
result.sequence > this.inSequence
|
|
|
|
) {
|
|
|
|
this.onHoverCallback(result.entity_id)
|
|
|
|
this.inSequence = result.sequence
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
// When the EngineConnection starts a connection, we want to register
|
|
|
|
// callbacks into the WebSocket/PeerConnection.
|
|
|
|
conn.websocket?.addEventListener('message', (event) => {
|
|
|
|
if (event.data instanceof ArrayBuffer) {
|
|
|
|
// If the data is an ArrayBuffer, it's the result of an export command,
|
|
|
|
// because in all other cases we send JSON strings. But in the case of
|
|
|
|
// export we send a binary blob.
|
|
|
|
// Pass this to our export function.
|
|
|
|
exportSave(event.data)
|
|
|
|
} else if (
|
|
|
|
typeof event.data === 'string' &&
|
|
|
|
event.data.toLocaleLowerCase().startsWith('error')
|
|
|
|
) {
|
|
|
|
console.warn('something went wrong: ', event.data)
|
|
|
|
} else {
|
|
|
|
const message: WebSocketResponse = JSON.parse(event.data)
|
|
|
|
|
|
|
|
if (message.type === 'modeling') {
|
|
|
|
const id = message.cmd_id
|
|
|
|
const command = this.artifactMap[id]
|
|
|
|
if ('ok' in message.result) {
|
|
|
|
const result: OkResponse = message.result.ok
|
|
|
|
if (result.type === 'select_with_point') {
|
|
|
|
if (result.entity_id) {
|
|
|
|
this.onClickCallback({
|
|
|
|
id: result.entity_id,
|
|
|
|
type: 'default',
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
this.onClickCallback()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (command && command.type === 'pending') {
|
|
|
|
const resolve = command.resolve
|
|
|
|
this.artifactMap[id] = {
|
|
|
|
type: 'result',
|
|
|
|
data: message.result,
|
|
|
|
}
|
|
|
|
resolve({
|
|
|
|
id,
|
2023-08-09 20:49:10 +10:00
|
|
|
})
|
|
|
|
} else {
|
2023-08-18 16:16:16 -04:00
|
|
|
this.artifactMap[id] = {
|
|
|
|
type: 'result',
|
|
|
|
data: message.result,
|
|
|
|
}
|
2023-08-09 20:49:10 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-08-18 16:16:16 -04:00
|
|
|
})
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
// TODO(paultag): this isn't quite right, and the double promises is
|
|
|
|
// pretty grim.
|
|
|
|
this.engineConnection?.waitForReady.then(this.resolveReady)
|
|
|
|
|
|
|
|
this.waitForReady.then(() => {
|
|
|
|
setIsStreamReady(true)
|
2023-06-22 16:43:33 +10:00
|
|
|
})
|
2023-08-18 16:16:16 -04:00
|
|
|
|
|
|
|
this.engineConnection?.connect()
|
2023-06-22 16:43:33 +10:00
|
|
|
}
|
2023-06-23 09:56:37 +10:00
|
|
|
tearDown() {
|
|
|
|
// close all channels, sockets and WebRTC connections
|
2023-08-18 16:16:16 -04:00
|
|
|
this.engineConnection?.close()
|
2023-06-23 09:56:37 +10:00
|
|
|
}
|
2023-06-22 16:43:33 +10:00
|
|
|
|
|
|
|
startNewSession() {
|
|
|
|
this.artifactMap = {}
|
|
|
|
this.sourceRangeMap = {}
|
|
|
|
}
|
|
|
|
endSession() {
|
2023-08-18 16:16:16 -04:00
|
|
|
// this.websocket?.close()
|
2023-06-22 16:43:33 +10:00
|
|
|
// socket.off('command')
|
|
|
|
}
|
|
|
|
onHover(callback: (id?: string) => void) {
|
|
|
|
// It's when the user hovers over a part in the 3d scene, and so the engine should tell the
|
|
|
|
// frontend about that (with it's id) so that the FE can highlight code associated with that id
|
|
|
|
this.onHoverCallback = callback
|
|
|
|
}
|
2023-08-09 20:49:10 +10:00
|
|
|
onClick(callback: (selection?: SelectionsArgs) => void) {
|
2023-06-22 16:43:33 +10:00
|
|
|
// It's when the user clicks on a part in the 3d scene, and so the engine should tell the
|
|
|
|
// frontend about that (with it's id) so that the FE can put the user's cursor on the right
|
|
|
|
// line of code
|
|
|
|
this.onClickCallback = callback
|
|
|
|
}
|
|
|
|
cusorsSelected(selections: {
|
|
|
|
otherSelections: Selections['otherSelections']
|
|
|
|
idBasedSelections: { type: string; id: string }[]
|
|
|
|
}) {
|
2023-08-18 16:16:16 -04:00
|
|
|
if (this.engineConnection?.websocket?.readyState === 0) {
|
2023-06-22 16:43:33 +10:00
|
|
|
console.log('socket not open')
|
|
|
|
return
|
|
|
|
}
|
2023-08-09 20:49:10 +10:00
|
|
|
this.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd: {
|
|
|
|
type: 'select_clear',
|
|
|
|
},
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
})
|
|
|
|
this.sendSceneCommand({
|
|
|
|
type: 'modeling_cmd_req',
|
|
|
|
cmd: {
|
|
|
|
type: 'select_add',
|
|
|
|
entities: selections.idBasedSelections.map((s) => s.id),
|
|
|
|
},
|
|
|
|
cmd_id: uuidv4(),
|
|
|
|
})
|
2023-06-22 16:43:33 +10:00
|
|
|
}
|
|
|
|
sendSceneCommand(command: EngineCommand) {
|
2023-08-18 16:16:16 -04:00
|
|
|
if (this.engineConnection?.websocket?.readyState === 0) {
|
2023-06-22 16:43:33 +10:00
|
|
|
console.log('socket not ready')
|
|
|
|
return
|
|
|
|
}
|
2023-08-18 08:12:32 -07:00
|
|
|
if (command.type !== 'modeling_cmd_req') return
|
2023-08-02 15:41:59 +10:00
|
|
|
const cmd = command.cmd
|
2023-08-18 16:16:16 -04:00
|
|
|
if (
|
|
|
|
cmd.type === 'camera_drag_move' &&
|
|
|
|
this.engineConnection?.lossyDataChannel
|
|
|
|
) {
|
2023-08-09 20:49:10 +10:00
|
|
|
cmd.sequence = this.outSequence
|
|
|
|
this.outSequence++
|
2023-08-18 16:16:16 -04:00
|
|
|
this.engineConnection?.lossyDataChannel?.send(JSON.stringify(command))
|
2023-08-09 20:49:10 +10:00
|
|
|
return
|
2023-08-18 16:16:16 -04:00
|
|
|
} else if (
|
|
|
|
cmd.type === 'highlight_set_entity' &&
|
|
|
|
this.engineConnection?.lossyDataChannel
|
|
|
|
) {
|
2023-08-09 20:49:10 +10:00
|
|
|
cmd.sequence = this.outSequence
|
|
|
|
this.outSequence++
|
2023-08-18 16:16:16 -04:00
|
|
|
this.engineConnection?.lossyDataChannel?.send(JSON.stringify(command))
|
2023-06-23 09:56:37 +10:00
|
|
|
return
|
|
|
|
}
|
2023-08-02 16:23:17 -07:00
|
|
|
console.log('sending command', command)
|
2023-08-18 16:16:16 -04:00
|
|
|
this.engineConnection?.websocket?.send(JSON.stringify(command))
|
2023-06-22 16:43:33 +10:00
|
|
|
}
|
|
|
|
sendModellingCommand({
|
|
|
|
id,
|
|
|
|
params,
|
|
|
|
range,
|
|
|
|
command,
|
|
|
|
}: {
|
|
|
|
id: string
|
|
|
|
params: any
|
|
|
|
range: SourceRange
|
|
|
|
command: EngineCommand
|
|
|
|
}): Promise<any> {
|
|
|
|
this.sourceRangeMap[id] = range
|
|
|
|
|
2023-08-18 16:16:16 -04:00
|
|
|
if (this.engineConnection?.websocket?.readyState === 0) {
|
2023-06-22 16:43:33 +10:00
|
|
|
console.log('socket not ready')
|
|
|
|
return new Promise(() => {})
|
|
|
|
}
|
2023-08-18 16:16:16 -04:00
|
|
|
this.engineConnection?.websocket?.send(JSON.stringify(command))
|
2023-06-22 16:43:33 +10:00
|
|
|
let resolve: (val: any) => void = () => {}
|
|
|
|
const promise = new Promise((_resolve, reject) => {
|
|
|
|
resolve = _resolve
|
|
|
|
})
|
|
|
|
this.artifactMap[id] = {
|
|
|
|
type: 'pending',
|
|
|
|
promise,
|
|
|
|
resolve,
|
|
|
|
}
|
|
|
|
return promise
|
|
|
|
}
|
|
|
|
commandResult(id: string): Promise<any> {
|
|
|
|
const command = this.artifactMap[id]
|
|
|
|
if (!command) {
|
|
|
|
throw new Error('No command found')
|
|
|
|
}
|
|
|
|
if (command.type === 'result') {
|
|
|
|
return command.data
|
|
|
|
}
|
|
|
|
return command.promise
|
|
|
|
}
|
|
|
|
async waitForAllCommands(): Promise<{
|
|
|
|
artifactMap: ArtifactMap
|
|
|
|
sourceRangeMap: SourceRangeMap
|
|
|
|
}> {
|
|
|
|
const pendingCommands = Object.values(this.artifactMap).filter(
|
|
|
|
({ type }) => type === 'pending'
|
|
|
|
) as PendingCommand[]
|
|
|
|
const proms = pendingCommands.map(({ promise }) => promise)
|
|
|
|
await Promise.all(proms)
|
|
|
|
return {
|
|
|
|
artifactMap: this.artifactMap,
|
|
|
|
sourceRangeMap: this.sourceRangeMap,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|