2023-10-07 23:57:47 +03:00
|
|
|
import { ObservableV2 } from 'lib0/observable'
|
|
|
|
import * as random from 'lib0/random'
|
2023-11-02 18:32:14 +03:00
|
|
|
import type { Path as LSPath } from 'shared/languageServerTypes'
|
2023-10-07 23:57:47 +03:00
|
|
|
import {
|
|
|
|
Builder,
|
|
|
|
ByteBuffer,
|
|
|
|
ChecksumBytesCommand,
|
|
|
|
ChecksumBytesReply,
|
|
|
|
EnsoUUID,
|
|
|
|
Error,
|
|
|
|
FileContentsReply,
|
|
|
|
FileSegment,
|
|
|
|
InboundMessage,
|
|
|
|
InboundPayload,
|
|
|
|
InitSessionCommand,
|
|
|
|
None,
|
|
|
|
OutboundMessage,
|
|
|
|
OutboundPayload,
|
2023-11-02 18:32:14 +03:00
|
|
|
Path,
|
2023-10-07 23:57:47 +03:00
|
|
|
ReadBytesCommand,
|
|
|
|
ReadBytesReply,
|
|
|
|
ReadFileCommand,
|
|
|
|
Success,
|
|
|
|
VisualizationUpdate,
|
|
|
|
WriteBytesCommand,
|
|
|
|
WriteBytesReply,
|
|
|
|
WriteFileCommand,
|
|
|
|
type Table,
|
|
|
|
} from './binaryProtocol'
|
2023-11-03 23:09:45 +03:00
|
|
|
import { uuidFromBits, uuidToBits } from './uuid'
|
2023-10-07 23:57:47 +03:00
|
|
|
import type { WebsocketClient } from './websocket'
|
|
|
|
import type { Uuid } from './yjsModel'
|
|
|
|
|
|
|
|
const PAYLOAD_CONSTRUCTOR = {
|
|
|
|
[OutboundPayload.NONE]: None,
|
|
|
|
[OutboundPayload.ERROR]: Error,
|
|
|
|
[OutboundPayload.SUCCESS]: Success,
|
|
|
|
[OutboundPayload.VISUALIZATION_UPDATE]: VisualizationUpdate,
|
|
|
|
[OutboundPayload.FILE_CONTENTS_REPLY]: FileContentsReply,
|
|
|
|
[OutboundPayload.WRITE_BYTES_REPLY]: WriteBytesReply,
|
|
|
|
[OutboundPayload.READ_BYTES_REPLY]: ReadBytesReply,
|
|
|
|
[OutboundPayload.CHECKSUM_BYTES_REPLY]: ChecksumBytesReply,
|
|
|
|
} satisfies Record<OutboundPayload, new () => Table>
|
|
|
|
|
|
|
|
export type DataServerEvents = {
|
2023-10-09 08:55:12 +03:00
|
|
|
[K in keyof typeof PAYLOAD_CONSTRUCTOR as `${K}`]: (
|
|
|
|
payload: InstanceType<(typeof PAYLOAD_CONSTRUCTOR)[K]>,
|
|
|
|
uuid: Uuid | null,
|
2023-10-07 23:57:47 +03:00
|
|
|
) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
export class DataServer extends ObservableV2<DataServerEvents> {
|
|
|
|
initialized = false
|
|
|
|
ready: Promise<void>
|
|
|
|
clientId!: string
|
|
|
|
resolveCallbacks = new Map<string, (data: any) => void>()
|
|
|
|
|
|
|
|
/** `websocket.binaryType` should be `ArrayBuffer`. */
|
|
|
|
constructor(public websocket: WebsocketClient) {
|
|
|
|
super()
|
|
|
|
if (websocket.connected) {
|
|
|
|
this.ready = Promise.resolve()
|
|
|
|
} else {
|
|
|
|
this.ready = new Promise((resolve, reject) => {
|
|
|
|
websocket.on('connect', () => resolve())
|
|
|
|
websocket.on('disconnect', reject)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
websocket.on('message', (rawPayload) => {
|
|
|
|
if (!(rawPayload instanceof ArrayBuffer)) {
|
|
|
|
console.warn('Data Server: Data type was invalid:', rawPayload)
|
|
|
|
// Ignore all non-binary messages. If the messages are `Blob`s instead, this is a
|
|
|
|
// misconfiguration and should also be ignored.
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const binaryMessage = OutboundMessage.getRootAsOutboundMessage(new ByteBuffer(rawPayload))
|
|
|
|
const payloadType = binaryMessage.payloadType()
|
|
|
|
const payload = binaryMessage.payload(new PAYLOAD_CONSTRUCTOR[payloadType]())
|
2023-11-03 23:09:45 +03:00
|
|
|
if (!payload) return
|
|
|
|
this.emit(`${payloadType}`, [payload, null])
|
|
|
|
const id = binaryMessage.correlationId()
|
|
|
|
if (id != null) {
|
|
|
|
const uuid = uuidFromBits(id.leastSigBits(), id.mostSigBits())
|
|
|
|
this.emit(`${payloadType}`, [payload, uuid])
|
|
|
|
const callback = this.resolveCallbacks.get(uuid)
|
|
|
|
callback?.(payload)
|
|
|
|
} else if (payload instanceof VisualizationUpdate) {
|
|
|
|
const id = payload.visualizationContext()?.visualizationId()
|
|
|
|
if (!id) return
|
|
|
|
const uuid = uuidFromBits(id.leastSigBits(), id.mostSigBits())
|
|
|
|
this.emit(`${payloadType}`, [payload, uuid])
|
2023-10-07 23:57:47 +03:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async initialize(clientId: Uuid) {
|
|
|
|
if (!this.initialized) {
|
|
|
|
this.clientId = clientId
|
|
|
|
await this.ready
|
|
|
|
await this.initSession()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected send<T = void>(
|
|
|
|
builder: Builder,
|
|
|
|
payloadType: InboundPayload,
|
|
|
|
payloadOffset: number,
|
|
|
|
): Promise<T> {
|
|
|
|
const messageUuid = random.uuidv4()
|
|
|
|
const rootTable = InboundMessage.createInboundMessage(
|
|
|
|
builder,
|
2023-11-03 23:09:45 +03:00
|
|
|
this.createUUID(messageUuid),
|
|
|
|
null /* correlation id */,
|
2023-10-07 23:57:47 +03:00
|
|
|
payloadType,
|
|
|
|
payloadOffset,
|
|
|
|
)
|
|
|
|
const promise = new Promise<T>((resolve) => {
|
|
|
|
this.resolveCallbacks.set(messageUuid, resolve)
|
|
|
|
})
|
|
|
|
this.websocket.send(builder.finish(rootTable).toArrayBuffer())
|
|
|
|
return promise
|
|
|
|
}
|
|
|
|
|
2023-11-03 23:09:45 +03:00
|
|
|
protected createUUID(uuid: string) {
|
2023-10-07 23:57:47 +03:00
|
|
|
const [leastSigBits, mostSigBits] = uuidToBits(uuid)
|
2023-11-03 23:09:45 +03:00
|
|
|
return (builder: Builder) => EnsoUUID.createEnsoUUID(builder, leastSigBits, mostSigBits)
|
2023-10-07 23:57:47 +03:00
|
|
|
}
|
|
|
|
|
2023-11-03 23:09:45 +03:00
|
|
|
initSession(): Promise<Success | Error> {
|
2023-10-07 23:57:47 +03:00
|
|
|
const builder = new Builder()
|
2023-11-03 23:09:45 +03:00
|
|
|
const commandOffset = InitSessionCommand.createInitSessionCommand(
|
|
|
|
builder,
|
|
|
|
this.createUUID(this.clientId),
|
|
|
|
)
|
|
|
|
return this.send(builder, InboundPayload.INIT_SESSION_CMD, commandOffset)
|
2023-10-07 23:57:47 +03:00
|
|
|
}
|
|
|
|
|
2023-11-03 23:09:45 +03:00
|
|
|
async writeFile(
|
|
|
|
path: LSPath,
|
|
|
|
contents: string | ArrayBuffer | Uint8Array,
|
|
|
|
): Promise<Success | Error> {
|
2023-10-07 23:57:47 +03:00
|
|
|
const builder = new Builder()
|
|
|
|
const contentsOffset = builder.createString(contents)
|
2023-11-03 23:09:45 +03:00
|
|
|
const segmentOffsets = path.segments.map((segment) => builder.createString(segment))
|
|
|
|
const segmentsOffset = Path.createSegmentsVector(builder, segmentOffsets)
|
|
|
|
const pathOffset = Path.createPath(builder, this.createUUID(path.rootId), segmentsOffset)
|
2023-10-07 23:57:47 +03:00
|
|
|
const command = WriteFileCommand.createWriteFileCommand(builder, pathOffset, contentsOffset)
|
|
|
|
return await this.send(builder, InboundPayload.WRITE_FILE_CMD, command)
|
|
|
|
}
|
|
|
|
|
2023-11-03 23:09:45 +03:00
|
|
|
async readFile(path: LSPath): Promise<FileContentsReply | Error> {
|
2023-10-07 23:57:47 +03:00
|
|
|
const builder = new Builder()
|
2023-11-03 23:09:45 +03:00
|
|
|
const segmentOffsets = path.segments.map((segment) => builder.createString(segment))
|
|
|
|
const segmentsOffset = Path.createSegmentsVector(builder, segmentOffsets)
|
|
|
|
const pathOffset = Path.createPath(builder, this.createUUID(path.rootId), segmentsOffset)
|
2023-10-07 23:57:47 +03:00
|
|
|
const command = ReadFileCommand.createReadFileCommand(builder, pathOffset)
|
|
|
|
return await this.send(builder, InboundPayload.READ_FILE_CMD, command)
|
|
|
|
}
|
|
|
|
|
|
|
|
async writeBytes(
|
2023-11-02 18:32:14 +03:00
|
|
|
path: LSPath,
|
2023-10-07 23:57:47 +03:00
|
|
|
index: bigint,
|
|
|
|
overwriteExisting: boolean,
|
|
|
|
contents: string | ArrayBuffer | Uint8Array,
|
2023-11-03 23:09:45 +03:00
|
|
|
): Promise<WriteBytesReply | Error> {
|
2023-10-07 23:57:47 +03:00
|
|
|
const builder = new Builder()
|
|
|
|
const bytesOffset = builder.createString(contents)
|
2023-11-03 23:09:45 +03:00
|
|
|
const segmentOffsets = path.segments.map((segment) => builder.createString(segment))
|
2023-11-02 18:32:14 +03:00
|
|
|
const segmentsOffset = Path.createSegmentsVector(builder, segmentOffsets)
|
2023-11-03 23:09:45 +03:00
|
|
|
const pathOffset = Path.createPath(builder, this.createUUID(path.rootId), segmentsOffset)
|
2023-10-07 23:57:47 +03:00
|
|
|
const command = WriteBytesCommand.createWriteBytesCommand(
|
|
|
|
builder,
|
|
|
|
pathOffset,
|
|
|
|
index,
|
|
|
|
overwriteExisting,
|
|
|
|
bytesOffset,
|
|
|
|
)
|
2023-11-03 23:09:45 +03:00
|
|
|
return await this.send(builder, InboundPayload.WRITE_BYTES_CMD, command)
|
2023-10-07 23:57:47 +03:00
|
|
|
}
|
|
|
|
|
2023-11-03 23:09:45 +03:00
|
|
|
async readBytes(path: LSPath, index: bigint, length: bigint): Promise<ReadBytesReply | Error> {
|
2023-10-07 23:57:47 +03:00
|
|
|
const builder = new Builder()
|
2023-11-03 23:09:45 +03:00
|
|
|
const segmentOffsets = path.segments.map((segment) => builder.createString(segment))
|
|
|
|
const segmentsOffset = Path.createSegmentsVector(builder, segmentOffsets)
|
|
|
|
const pathOffset = Path.createPath(builder, this.createUUID(path.rootId), segmentsOffset)
|
2023-10-07 23:57:47 +03:00
|
|
|
const segmentOffset = FileSegment.createFileSegment(builder, pathOffset, index, length)
|
|
|
|
const command = ReadBytesCommand.createReadBytesCommand(builder, segmentOffset)
|
2023-11-03 23:09:45 +03:00
|
|
|
return await this.send(builder, InboundPayload.READ_BYTES_CMD, command)
|
2023-10-07 23:57:47 +03:00
|
|
|
}
|
|
|
|
|
2023-11-03 23:09:45 +03:00
|
|
|
async checksumBytes(
|
|
|
|
path: LSPath,
|
|
|
|
index: bigint,
|
|
|
|
length: bigint,
|
|
|
|
): Promise<ChecksumBytesReply | Error> {
|
2023-10-07 23:57:47 +03:00
|
|
|
const builder = new Builder()
|
2023-11-03 23:09:45 +03:00
|
|
|
const segmentOffsets = path.segments.map((segment) => builder.createString(segment))
|
|
|
|
const segmentsOffset = Path.createSegmentsVector(builder, segmentOffsets)
|
|
|
|
const pathOffset = Path.createPath(builder, this.createUUID(path.rootId), segmentsOffset)
|
2023-10-07 23:57:47 +03:00
|
|
|
const segmentOffset = FileSegment.createFileSegment(builder, pathOffset, index, length)
|
|
|
|
const command = ChecksumBytesCommand.createChecksumBytesCommand(builder, segmentOffset)
|
2023-11-03 23:09:45 +03:00
|
|
|
return await this.send(builder, InboundPayload.WRITE_BYTES_CMD, command)
|
2023-10-07 23:57:47 +03:00
|
|
|
}
|
|
|
|
}
|