mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 00:11:45 +03:00
de406c69fa
Fixes #8520 If the websocket is closed not by us, we automatically try to reconnect with it, and initialize the protocol again. **Restoring state (execution contexts, attached visualizations) is not part of this PR**. It's a part of making IDE work after hibernation (or LS crash). # Important Notes It required somewhat heavy refactoring: 1. I decided to use an existing implementation of reconnecting websocket. Replaced (later discovered by me) our implementation. 2. The LanguageServer class now handles both reconnecting and re-initializing - that make usage of it simpler (no more `Promise<LanguageServer>` - each method will just wait for (re)connection and initialization. 3. The stuff in `net` src's module was partially moved to shared's counterpart (with tests). Merged `exponentialBackoff` implementations, which also brought me to 4. Rewriting LS client, so it returns Result instead of throwing, what is closer our desired state, and allows us using exponentialBackoff method without any wrappers.
208 lines
7.0 KiB
TypeScript
208 lines
7.0 KiB
TypeScript
import { createSHA3 } from 'hash-wasm'
|
|
import * as random from 'lib0/random'
|
|
import {
|
|
Builder,
|
|
ByteBuffer,
|
|
ChecksumBytesCommand,
|
|
ChecksumBytesReply,
|
|
EnsoDigest,
|
|
EnsoUUID,
|
|
ErrorPayload,
|
|
Error as ErrorResponse,
|
|
FileContentsReply,
|
|
InboundMessage,
|
|
InboundPayload,
|
|
InitSessionCommand,
|
|
None,
|
|
Null,
|
|
OutboundMessage,
|
|
OutboundPayload,
|
|
Path,
|
|
ReadBytesCommand,
|
|
ReadBytesReply,
|
|
ReadFileCommand,
|
|
Success,
|
|
WriteBytesCommand,
|
|
WriteFileCommand,
|
|
type AnyOutboundPayload,
|
|
type Offset,
|
|
type Table,
|
|
} from 'shared/binaryProtocol'
|
|
import { LanguageServerErrorCode } from 'shared/languageServerTypes'
|
|
import { uuidToBits } from 'shared/uuid'
|
|
|
|
const sha3 = createSHA3(224)
|
|
|
|
function pathSegments(path: Path) {
|
|
return Array.from({ length: path.segmentsLength() }, (_, i) => path.segments(i))
|
|
}
|
|
|
|
function createError(builder: Builder, code: LanguageServerErrorCode, message: string) {
|
|
const messageOffset = builder.createString(message)
|
|
return {
|
|
type: OutboundPayload.ERROR,
|
|
offset: ErrorResponse.createError(builder, code, messageOffset, ErrorPayload.NONE, Null),
|
|
}
|
|
}
|
|
|
|
function createMessageId(builder: Builder) {
|
|
const messageUuid = random.uuidv4()
|
|
const [leastSigBits, mostSigBits] = uuidToBits(messageUuid)
|
|
return EnsoUUID.createEnsoUUID(builder, leastSigBits, mostSigBits)
|
|
}
|
|
|
|
function createCorrelationId(messageId: EnsoUUID) {
|
|
return (builder: Builder) =>
|
|
EnsoUUID.createEnsoUUID(builder, messageId.leastSigBits(), messageId.mostSigBits())
|
|
}
|
|
|
|
const PAYLOAD_CONSTRUCTOR = {
|
|
[InboundPayload.NONE]: None,
|
|
[InboundPayload.INIT_SESSION_CMD]: InitSessionCommand,
|
|
[InboundPayload.WRITE_FILE_CMD]: WriteFileCommand,
|
|
[InboundPayload.READ_FILE_CMD]: ReadFileCommand,
|
|
[InboundPayload.WRITE_BYTES_CMD]: WriteBytesCommand,
|
|
[InboundPayload.READ_BYTES_CMD]: ReadBytesCommand,
|
|
[InboundPayload.CHECKSUM_BYTES_CMD]: ChecksumBytesCommand,
|
|
} satisfies Record<InboundPayload, new () => Table>
|
|
|
|
export function mockDataWSHandler(
|
|
readFile: (segments: string[]) => Promise<ArrayBuffer | null | undefined>,
|
|
cb?: (send: (data: string | Blob | ArrayBufferLike | ArrayBufferView) => void) => void,
|
|
) {
|
|
let sentSend = false
|
|
return async (
|
|
message: string | Blob | ArrayBufferLike | ArrayBufferView,
|
|
send: (data: string | Blob | ArrayBufferLike | ArrayBufferView) => void,
|
|
) => {
|
|
if (!sentSend) cb?.(send)
|
|
sentSend = true
|
|
if (!(message instanceof ArrayBuffer)) return
|
|
const binaryMessage = InboundMessage.getRootAsInboundMessage(new ByteBuffer(message))
|
|
const payloadType = binaryMessage.payloadType()
|
|
const payload = binaryMessage.payload(new PAYLOAD_CONSTRUCTOR[payloadType]())
|
|
if (!payload) return
|
|
const builder = new Builder()
|
|
let response: { type: OutboundPayload; offset: Offset<AnyOutboundPayload> } | undefined
|
|
switch (payloadType) {
|
|
case InboundPayload.NONE: {
|
|
response = {
|
|
type: OutboundPayload.NONE,
|
|
offset: Null,
|
|
}
|
|
break
|
|
}
|
|
case InboundPayload.INIT_SESSION_CMD: {
|
|
response = {
|
|
type: OutboundPayload.SUCCESS,
|
|
offset: Success.createSuccess(builder),
|
|
}
|
|
break
|
|
}
|
|
case InboundPayload.WRITE_FILE_CMD: {
|
|
response = createError(
|
|
builder,
|
|
LanguageServerErrorCode.AccessDenied,
|
|
'Cannot WriteFile to a read-only mock.',
|
|
)
|
|
break
|
|
}
|
|
case InboundPayload.READ_FILE_CMD: {
|
|
const payload_ = payload as ReadFileCommand
|
|
const path = payload_.path()
|
|
if (!path) {
|
|
response = createError(builder, LanguageServerErrorCode.NotFile, 'Invalid Path')
|
|
break
|
|
}
|
|
const file = await readFile(pathSegments(path))
|
|
if (!file) {
|
|
response = createError(builder, LanguageServerErrorCode.FileNotFound, 'File not found')
|
|
break
|
|
}
|
|
const contentOffset = builder.createString(file)
|
|
response = {
|
|
type: OutboundPayload.FILE_CONTENTS_REPLY,
|
|
offset: FileContentsReply.createFileContentsReply(builder, contentOffset),
|
|
}
|
|
break
|
|
}
|
|
case InboundPayload.WRITE_BYTES_CMD: {
|
|
response = createError(
|
|
builder,
|
|
LanguageServerErrorCode.AccessDenied,
|
|
'Cannot WriteBytes to a read-only mock.',
|
|
)
|
|
break
|
|
}
|
|
case InboundPayload.READ_BYTES_CMD: {
|
|
const payload_ = payload as ReadBytesCommand
|
|
const segment = payload_.segment()
|
|
const path = segment && segment.path()
|
|
if (!path) {
|
|
response = createError(
|
|
builder,
|
|
LanguageServerErrorCode.NotFile,
|
|
'Invalid FileSegment or Path',
|
|
)
|
|
break
|
|
}
|
|
const file = await readFile(pathSegments(path))
|
|
if (!file) {
|
|
response = createError(builder, LanguageServerErrorCode.FileNotFound, 'File not found')
|
|
break
|
|
}
|
|
const start = Number(segment.byteOffset())
|
|
const slice = file.slice(start, start + Number(segment.length()))
|
|
const contentOffset = builder.createString(slice)
|
|
const digest = (await sha3).init().update(new Uint8Array(slice)).digest('binary')
|
|
const checksumBytesOffset = EnsoDigest.createBytesVector(builder, digest)
|
|
const checksumOffset = EnsoDigest.createEnsoDigest(builder, checksumBytesOffset)
|
|
response = {
|
|
type: OutboundPayload.READ_BYTES_REPLY,
|
|
offset: ReadBytesReply.createReadBytesReply(builder, checksumOffset, contentOffset),
|
|
}
|
|
break
|
|
}
|
|
case InboundPayload.CHECKSUM_BYTES_CMD: {
|
|
const payload_ = payload as ChecksumBytesCommand
|
|
const segment = payload_.segment()
|
|
const path = segment && segment.path()
|
|
if (!path) {
|
|
response = createError(
|
|
builder,
|
|
LanguageServerErrorCode.NotFile,
|
|
'Invalid FileSegment or Path',
|
|
)
|
|
break
|
|
}
|
|
const file = await readFile(pathSegments(path))
|
|
if (!file) {
|
|
response = createError(builder, LanguageServerErrorCode.FileNotFound, 'File not found')
|
|
break
|
|
}
|
|
const start = Number(segment.byteOffset())
|
|
const slice = file.slice(start, start + Number(segment.length()))
|
|
const digest = (await sha3).init().update(new Uint8Array(slice)).digest('binary')
|
|
const bytesOffset = EnsoDigest.createBytesVector(builder, digest)
|
|
const checksumOffset = EnsoDigest.createEnsoDigest(builder, bytesOffset)
|
|
response = {
|
|
type: OutboundPayload.CHECKSUM_BYTES_REPLY,
|
|
offset: ChecksumBytesReply.createChecksumBytesReply(builder, checksumOffset),
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if (!response) return
|
|
const correlationUuid = binaryMessage.messageId()
|
|
if (!correlationUuid) return
|
|
const rootTable = OutboundMessage.createOutboundMessage(
|
|
builder,
|
|
createMessageId,
|
|
createCorrelationId(correlationUuid),
|
|
response.type,
|
|
response.offset,
|
|
)
|
|
send(builder.finish(rootTable).toArrayBuffer())
|
|
}
|
|
}
|