UNPKG

@vscode/sync-api-common

Version:

An RPC implementation between Web and NodeJS workers that works sync

735 lines (734 loc) 28.2 kB
"use strict"; /* -------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseServiceConnection = exports.RequestResult = exports.BaseClientConnection = exports.RPCError = exports.RPCErrno = exports.VariableResult = exports.Int64Result = exports.Uint64Result = exports.Int32Result = exports.Uint32Result = exports.Int16Result = exports.Uint16Result = exports.Int8Result = exports.Uint8Result = exports.Notification = exports.Request = void 0; const ral_1 = __importDefault(require("./ral")); var TypedArray; (function (TypedArray) { function is(value) { return value instanceof Uint8Array || value instanceof Int8Array || value instanceof Uint16Array || value instanceof Int16Array || value instanceof Uint32Array || value instanceof Int32Array || value instanceof BigUint64Array || value instanceof BigInt64Array; } TypedArray.is = is; function set(sharedArrayBuffer, offset, data) { if (data instanceof Uint8Array) { new Uint8Array(sharedArrayBuffer, offset, data.length).set(data); } else if (data instanceof Int8Array) { new Int8Array(sharedArrayBuffer, offset, data.length).set(data); } else if (data instanceof Uint16Array) { new Uint16Array(sharedArrayBuffer, offset, data.length).set(data); } else if (data instanceof Int16Array) { new Int16Array(sharedArrayBuffer, offset, data.length).set(data); } else if (data instanceof Uint32Array) { new Uint32Array(sharedArrayBuffer, offset, data.length).set(data); } else if (data instanceof Int32Array) { new Int32Array(sharedArrayBuffer, offset, data.length).set(data); } else if (data instanceof BigUint64Array) { new BigUint64Array(sharedArrayBuffer, offset, data.length).set(data); } else if (data instanceof BigInt64Array) { new BigInt64Array(sharedArrayBuffer, offset, data.length).set(data); } else { throw new Error(`Unknown type array type`); } } TypedArray.set = set; })(TypedArray || (TypedArray = {})); var Request; (function (Request) { function is(value) { const candidate = value; return candidate !== undefined && candidate !== null && typeof candidate.method === 'string'; } Request.is = is; })(Request || (exports.Request = Request = {})); var Notification; (function (Notification) { function is(value) { const candidate = value; return candidate !== undefined && candidate !== null && typeof candidate.method === 'string' && typeof candidate.id === 'undefined'; } Notification.is = is; })(Notification || (exports.Notification = Notification = {})); class NoResult { static kind = 0; constructor() { } get kind() { return NoResult.kind; } get byteLength() { return 0; } get length() { return 0; } getPadding(_offset) { return 0; } createResultArray(sharedArrayBuffer, offset) { return new Uint8Array(sharedArrayBuffer, offset, 0); } } class Uint8Result { static kind = 1; #length; static fromLength(length) { return new Uint8Result(length); } static fromByteLength(byteLength) { return new Uint8Result(byteLength); } constructor(value) { this.#length = value; } get kind() { return Uint8Result.kind; } get byteLength() { return this.#length * Uint8Array.BYTES_PER_ELEMENT; } get length() { return this.#length; } getPadding(offset) { return TypedArrayResult.getPadding(offset); } createResultArray(sharedArrayBuffer, offset) { return new Uint8Array(sharedArrayBuffer, offset, this.length); } is(value) { return value instanceof Uint8Array; } } exports.Uint8Result = Uint8Result; class Int8Result { static kind = 2; #length; static fromLength(length) { return new Int8Result(length); } static fromByteLength(byteLength) { return new Int8Result(byteLength); } constructor(value) { this.#length = value; } get kind() { return Int8Result.kind; } get byteLength() { return this.#length * Int8Array.BYTES_PER_ELEMENT; } get length() { return this.#length; } getPadding(offset) { return TypedArrayResult.getPadding(offset); } createResultArray(sharedArrayBuffer, offset) { return new Int8Array(sharedArrayBuffer, offset, this.length); } is(value) { return value instanceof Int8Array; } } exports.Int8Result = Int8Result; class Uint16Result { static kind = 3; #length; static fromLength(length) { return new Uint16Result(length); } static fromByteLength(byteLength) { if (byteLength % Uint16Array.BYTES_PER_ELEMENT !== 0) { throw new Error(`Byte length must be a multiple of ${Uint16Array.BYTES_PER_ELEMENT} but was ${byteLength}`); } return new Uint16Result(byteLength / Uint16Array.BYTES_PER_ELEMENT); } constructor(value) { this.#length = value; } get kind() { return Uint16Result.kind; } get byteLength() { return this.#length * Uint16Array.BYTES_PER_ELEMENT; } get length() { return this.#length; } getPadding(offset) { return TypedArrayResult.getPadding(offset); } createResultArray(sharedArrayBuffer, offset) { return new Uint16Array(sharedArrayBuffer, offset, this.length); } is(value) { return value instanceof Uint16Array; } } exports.Uint16Result = Uint16Result; class Int16Result { static kind = 4; #length; static fromLength(length) { return new Int16Result(length); } static fromByteLength(byteLength) { if (byteLength % Int16Array.BYTES_PER_ELEMENT !== 0) { throw new Error(`Byte length must be a multiple of ${Int16Array.BYTES_PER_ELEMENT} but was ${byteLength}`); } return new Int16Result(byteLength / Int16Array.BYTES_PER_ELEMENT); } constructor(value) { this.#length = value; } get kind() { return Int16Result.kind; } get byteLength() { return this.#length * Int16Array.BYTES_PER_ELEMENT; } get length() { return this.#length; } getPadding(offset) { return TypedArrayResult.getPadding(offset); } createResultArray(sharedArrayBuffer, offset) { return new Int16Array(sharedArrayBuffer, offset, this.length); } is(value) { return value instanceof Int16Array; } } exports.Int16Result = Int16Result; class Uint32Result { static kind = 5; #length; static fromLength(length) { return new Uint32Result(length); } static fromByteLength(byteLength) { if (byteLength % Uint32Array.BYTES_PER_ELEMENT !== 0) { throw new Error(`Byte length must be a multiple of ${Uint32Array.BYTES_PER_ELEMENT} but was ${byteLength}`); } return new Uint32Result(byteLength / Uint32Array.BYTES_PER_ELEMENT); } constructor(value) { this.#length = value; } get kind() { return Uint32Result.kind; } get byteLength() { return this.#length * Uint32Array.BYTES_PER_ELEMENT; } get length() { return this.#length; } getPadding(offset) { return TypedArrayResult.getPadding(offset); } createResultArray(sharedArrayBuffer, offset) { return new Uint32Array(sharedArrayBuffer, offset, this.length); } is(value) { return value instanceof Uint32Array; } } exports.Uint32Result = Uint32Result; class Int32Result { static kind = 6; #length; static fromLength(length) { return new Int32Result(length); } static fromByteLength(byteLength) { if (byteLength % Int32Array.BYTES_PER_ELEMENT !== 0) { throw new Error(`Byte length must be a multiple of ${Int32Array.BYTES_PER_ELEMENT} but was ${byteLength}`); } return new Int32Result(byteLength / Int32Array.BYTES_PER_ELEMENT); } constructor(value) { this.#length = value; } get kind() { return Int32Result.kind; } get byteLength() { return this.#length * Int32Array.BYTES_PER_ELEMENT; } get length() { return this.#length; } getPadding(offset) { return TypedArrayResult.getPadding(offset); } createResultArray(sharedArrayBuffer, offset) { return new Int32Array(sharedArrayBuffer, offset, this.length); } is(value) { return value instanceof Int32Array; } } exports.Int32Result = Int32Result; class Uint64Result { static kind = 7; #length; static fromLength(length) { return new Uint64Result(length); } static fromByteLength(byteLength) { if (byteLength % BigUint64Array.BYTES_PER_ELEMENT !== 0) { throw new Error(`Byte length must be a multiple of ${BigUint64Array.BYTES_PER_ELEMENT} but was ${byteLength}`); } return new Uint64Result(byteLength / BigUint64Array.BYTES_PER_ELEMENT); } constructor(value) { this.#length = value; } get kind() { return Uint64Result.kind; } get byteLength() { return this.#length * BigUint64Array.BYTES_PER_ELEMENT; } get length() { return this.#length; } getPadding(offset) { return BigUint64Array.BYTES_PER_ELEMENT - (offset % BigUint64Array.BYTES_PER_ELEMENT); } createResultArray(sharedArrayBuffer, offset) { return new BigUint64Array(sharedArrayBuffer, offset, this.length); } is(value) { return value instanceof BigUint64Array; } } exports.Uint64Result = Uint64Result; class Int64Result { static kind = 8; #length; static fromLength(length) { return new Int64Result(length); } static fromByteLength(byteLength) { if (byteLength % BigInt64Array.BYTES_PER_ELEMENT !== 0) { throw new Error(`Byte length must be a multiple of ${BigInt64Array.BYTES_PER_ELEMENT} but was ${byteLength}`); } return new Int64Result(byteLength / BigInt64Array.BYTES_PER_ELEMENT); } constructor(value) { this.#length = value; } get kind() { return Int64Result.kind; } get byteLength() { return this.#length * BigInt64Array.BYTES_PER_ELEMENT; } get length() { return this.#length; } getPadding(offset) { return BigInt64Array.BYTES_PER_ELEMENT - (offset % BigInt64Array.BYTES_PER_ELEMENT); } createResultArray(sharedArrayBuffer, offset) { return new BigInt64Array(sharedArrayBuffer, offset, this.length); } is(value) { return value instanceof BigInt64Array; } } exports.Int64Result = Int64Result; class VariableResult { static kind = 9; #mode; constructor(mode) { this.#mode = mode; } get kind() { return VariableResult.kind; } get mode() { return this.#mode; } get byteLength() { return 0; } get length() { return 0; } getPadding(_offset) { return 0; } } exports.VariableResult = VariableResult; var TypedArrayResult; (function (TypedArrayResult) { function fromByteLength(kind, byteLength) { switch (kind) { case Uint8Result.kind: return Uint8Result.fromByteLength(byteLength); case Int8Result.kind: return Int8Result.fromByteLength(byteLength); case Uint16Result.kind: return Uint16Result.fromByteLength(byteLength); case Int16Result.kind: return Int16Result.fromByteLength(byteLength); case Uint32Result.kind: return Uint32Result.fromByteLength(byteLength); case Int32Result.kind: return Int32Result.fromByteLength(byteLength); case Uint64Result.kind: return Uint64Result.fromByteLength(byteLength); case Int64Result.kind: return Int64Result.fromByteLength(byteLength); case VariableResult.kind: // send another request to get the result. throw new Error(`No result array for variable results result type.`); default: throw new Error(`Unknown result kind ${kind}`); } } TypedArrayResult.fromByteLength = fromByteLength; /** * For now we align everything on a 4 byte boundary */ function getPadding(offset) { return 4 - (offset % 4); } TypedArrayResult.getPadding = getPadding; })(TypedArrayResult || (TypedArrayResult = {})); var ResultType; (function (ResultType) { function is(value) { return value instanceof Uint8Result || value instanceof Int8Result || value instanceof Uint16Result || value instanceof Int16Result || value instanceof Uint32Result || value instanceof Int32Result || value instanceof Uint64Result || value instanceof Int64Result || value instanceof VariableResult || value instanceof NoResult; } ResultType.is = is; })(ResultType || (ResultType = {})); /** * Errno numbers specific to the sync RPC calls. We start at 16384 to not collide * with POSIX error numbers. */ var RPCErrno; (function (RPCErrno) { /** * No error occurred. RPC call completed successfully. */ RPCErrno.Success = 0; /** * The sync RPC called timed out. */ RPCErrno.TimedOut = 1; /** * A unknown error has occurred. */ RPCErrno.UnknownError = 16384; /** * Fetching the lazy result from the service failed. */ RPCErrno.LazyResultFailed = RPCErrno.UnknownError + 1; /** * No handler on the service side found. */ RPCErrno.NoHandlerFound = RPCErrno.LazyResultFailed + 1; /** * Invalid message format. */ RPCErrno.InvalidMessageFormat = RPCErrno.NoHandlerFound + 1; /** * Start of the custom error number range. */ RPCErrno.$Custom = 32768; })(RPCErrno || (exports.RPCErrno = RPCErrno = {})); class RPCError extends Error { errno; constructor(errno, message) { super(message); this.errno = errno; } } exports.RPCError = RPCError; class BaseClientConnection { id; textEncoder; textDecoder; readyPromise; readyCallbacks; constructor() { this.id = 1; this.textEncoder = (0, ral_1.default)().TextEncoder.create(); this.textDecoder = (0, ral_1.default)().TextDecoder.create(); this.readyPromise = new Promise((resolve, reject) => { this.readyCallbacks = { resolve, reject }; }); } serviceReady() { return this.readyPromise; } sendRequest = this._sendRequest; _sendRequest(method, arg1, arg2, arg3) { const id = this.id++; const request = { id: id, method }; let params = undefined; let resultType = new NoResult(); let timeout = undefined; if (ResultType.is(arg1)) { resultType = arg1; } else if (typeof arg1 === 'number') { timeout = arg1; } else if (arg1 !== undefined || arg1 !== null) { params = arg1; } if (typeof arg2 === 'number') { timeout = arg2; } else if (arg2 !== undefined) { resultType = arg2; } if (typeof arg3 === 'number') { timeout = arg3; } if (params !== undefined) { request.params = {}; for (const property of Object.keys(params)) { if (property !== 'binary') { request.params[property] = params[property]; } else { request.params['binary'] = null; } } } const requestData = this.textEncoder.encode(JSON.stringify(request, undefined, 0)); const binaryData = params?.binary; const binaryDataLength = binaryData !== undefined ? binaryData.byteLength : 0; const requestOffset = 4 /* SyncSize.total */ + 32 /* HeaderSize.total */; const binaryOffset = requestOffset + requestData.byteLength; const resultByteLength = resultType.byteLength; const resultPadding = resultType.getPadding(binaryOffset + binaryDataLength); const resultOffset = (binaryOffset + binaryDataLength) + resultPadding; const sharedArrayBufferLength = 4 /* SyncSize.total */ + 32 /* HeaderSize.total */ + requestData.byteLength + binaryDataLength + resultPadding + resultByteLength; const sharedArrayBuffer = new SharedArrayBuffer(sharedArrayBufferLength); const header = new Uint32Array(sharedArrayBuffer, 4 /* SyncSize.total */, 32 /* HeaderSize.total */ / 4); header[0 /* HeaderIndex.messageOffset */] = requestOffset; header[1 /* HeaderIndex.messageByteLength */] = requestData.byteLength; header[2 /* HeaderIndex.binaryParamOffset */] = binaryOffset; header[3 /* HeaderIndex.binaryParamByteLength */] = binaryDataLength; header[4 /* HeaderIndex.errno */] = RPCErrno.Success; header[5 /* HeaderIndex.resultKind */] = resultType.kind; header[6 /* HeaderIndex.resultOffset */] = resultOffset; header[7 /* HeaderIndex.resultByteLength */] = resultByteLength; const raw = new Uint8Array(sharedArrayBuffer); raw.set(requestData, requestOffset); if (binaryData !== undefined) { raw.set(binaryData, binaryOffset); } // Send the shard array buffer to the other worker const sync = new Int32Array(sharedArrayBuffer, 0, 1); Atomics.store(sync, 0, 0); // Send the shared array buffer to the extension host worker this.postMessage(sharedArrayBuffer); // Wait for the answer const result = Atomics.wait(sync, 0, 0, timeout); switch (result) { case 'timed-out': return { errno: RPCErrno.TimedOut }; case 'not-equal': const value = Atomics.load(sync, 0); // If the value === 1 the service has already // provided the result. Otherwise we actually // don't know what happened :-(. if (value !== 1) { return { errno: RPCErrno.UnknownError }; } } const errno = header[4 /* HeaderIndex.errno */]; if (errno !== 0) { return { errno }; } else { switch (resultType.kind) { case NoResult.kind: return { errno: 0 }; case VariableResult.kind: const lazyResultLength = header[7 /* HeaderIndex.resultByteLength */]; if (lazyResultLength === 0) { return { errno: 0, data: resultType.mode === 'binary' ? new Uint8Array(0) : '' }; } const lazyResult = this._sendRequest('$/fetchResult', { resultId: id }, Uint8Result.fromLength(lazyResultLength), timeout); if (lazyResult.errno !== 0) { return { errno: lazyResult.errno }; } if (RequestResult.hasData(lazyResult)) { try { // We need to slice the Uint8Array we received since it is a view onto a // shared array buffer and in the browser the text decoder throws when // reading from a Uint8Array. Since this seems to be the correct behavior // anyways (the array could otherwise change underneath) we do the same // for NodeJS const data = resultType.mode === 'binary' ? lazyResult.data : JSON.parse(this.textDecoder.decode(lazyResult.data.slice())); return { errno: 0, data }; } catch (error) { (0, ral_1.default)().console.error(error); return { errno: RPCErrno.LazyResultFailed }; } } else { return { errno: RPCErrno.LazyResultFailed }; } default: return { errno: 0, data: resultType.createResultArray(sharedArrayBuffer, resultOffset) }; } } } handleMessage(message) { if (message.method === '$/ready') { this.readyCallbacks.resolve(message.params); } } } exports.BaseClientConnection = BaseClientConnection; var RequestResult; (function (RequestResult) { function hasData(value) { const candidate = value; return candidate.errno === 0 && candidate.data !== undefined; } RequestResult.hasData = hasData; })(RequestResult || (exports.RequestResult = RequestResult = {})); class BaseServiceConnection { textDecoder; textEncoder; requestHandlers; requestResults; constructor() { this.textDecoder = (0, ral_1.default)().TextDecoder.create(); this.textEncoder = (0, ral_1.default)().TextEncoder.create(); this.requestHandlers = new Map(); this.requestResults = new Map(); } onRequest = this._onRequest; _onRequest(method, handler) { this.requestHandlers.set(method, handler); return { dispose: () => this.requestHandlers.delete(method) }; } async handleMessage(sharedArrayBuffer) { const header = new Uint32Array(sharedArrayBuffer, 4 /* SyncSize.total */, 32 /* HeaderSize.total */ / 4); const requestOffset = header[0 /* HeaderIndex.messageOffset */]; const requestLength = header[1 /* HeaderIndex.messageByteLength */]; try { // See above why we need to slice the Uint8Array. const message = JSON.parse(this.textDecoder.decode(new Uint8Array(sharedArrayBuffer, requestOffset, requestLength).slice())); if (Request.is(message)) { if (message.method === '$/fetchResult') { const resultId = message.params.resultId; const result = this.requestResults.get(resultId); this.requestResults.delete(resultId); const resultOffset = header[6 /* HeaderIndex.resultOffset */]; const resultByteLength = header[7 /* HeaderIndex.resultByteLength */]; if (result !== undefined && result.byteLength === resultByteLength) { TypedArray.set(sharedArrayBuffer, resultOffset, result); header[4 /* HeaderIndex.errno */] = RPCErrno.Success; } else { header[4 /* HeaderIndex.errno */] = RPCErrno.LazyResultFailed; } } else { if (message.params?.binary === null) { const binaryParamsLength = header[3 /* HeaderIndex.binaryParamByteLength */]; const binaryParamsOffset = header[2 /* HeaderIndex.binaryParamOffset */]; const binary = new Uint8Array(sharedArrayBuffer, binaryParamsOffset, binaryParamsLength); message.params = message.params ?? {}; message.params.binary = binary; } const handler = this.requestHandlers.get(message.method); if (handler !== undefined) { const resultKind = header[5 /* HeaderIndex.resultKind */]; const resultOffset = header[6 /* HeaderIndex.resultOffset */]; const resultByteLength = header[7 /* HeaderIndex.resultByteLength */]; let handlerResult; let requestResult; switch (resultKind) { case NoResult.kind: handlerResult = message.params !== undefined ? handler(message.params) : handler(); requestResult = handlerResult instanceof Promise ? await handlerResult : handlerResult; header[4 /* HeaderIndex.errno */] = requestResult.errno; break; case VariableResult.kind: handlerResult = message.params !== undefined ? handler(message.params) : handler(); requestResult = handlerResult instanceof Promise ? await handlerResult : handlerResult; header[4 /* HeaderIndex.errno */] = requestResult.errno; if (requestResult.errno === 0) { if (RequestResult.hasData(requestResult)) { const data = requestResult.data; const buffer = TypedArray.is(data) ? data : this.textEncoder.encode(JSON.stringify(data, undefined, 0)); header[7 /* HeaderIndex.resultByteLength */] = buffer.byteLength; // We only need to keep a result which is greater than zero. // If it is zero we can create an empty array on the other side. if (buffer.byteLength > 0) { this.requestResults.set(message.id, buffer); } } } break; default: const typedResult = TypedArrayResult.fromByteLength(resultKind, resultByteLength); const resultBuffer = typedResult.createResultArray(sharedArrayBuffer, resultOffset); handlerResult = message.params !== undefined ? handler(message.params, resultBuffer) : handler(resultBuffer); requestResult = handlerResult instanceof Promise ? await handlerResult : handlerResult; header[4 /* HeaderIndex.errno */] = requestResult.errno; } } else { header[4 /* HeaderIndex.errno */] = RPCErrno.NoHandlerFound; } } } else { header[4 /* HeaderIndex.errno */] = RPCErrno.InvalidMessageFormat; } } catch (error) { (0, ral_1.default)().console.error(error); header[4 /* HeaderIndex.errno */] = RPCErrno.UnknownError; } const sync = new Int32Array(sharedArrayBuffer, 0, 1); Atomics.store(sync, 0, 1); Atomics.notify(sync, 0); } signalReady(params) { const notification = { method: '$/ready', params }; this.postMessage(notification); } } exports.BaseServiceConnection = BaseServiceConnection;