Skip to content

Commit

Permalink
simplify rpc logic + improve typings
Browse files Browse the repository at this point in the history
  • Loading branch information
paul-marechal committed Nov 9, 2021
1 parent fa275b4 commit e179450
Show file tree
Hide file tree
Showing 19 changed files with 127 additions and 81 deletions.
9 changes: 9 additions & 0 deletions packages/core/src/common/cancellation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ export namespace CancellationToken {
isCancellationRequested: true,
onCancellationRequested: shortcutEvent
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function Is(what: any): what is CancellationToken {
return typeof what === 'object'
// eslint-disable-next-line no-null/no-null
&& what !== null
&& typeof what.onCancellationRequested === 'function'
&& typeof what.isCancellationRequested === 'boolean';
}
}

export class CancellationError extends Error {
Expand Down
36 changes: 36 additions & 0 deletions packages/core/src/common/messaging/json-rpc-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/********************************************************************************
* Copyright (C) 2021 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { MaybePromise, Unpromise } from '../types';

export type UndefinedOrNull<T> = T extends object
? { [K in keyof T]: UndefinedOrNull<T[K]> }
: T extends undefined ? undefined | null : T;

export interface JsonRpcMethods {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[method: string]: (...args: any[]) => MaybePromise<any>
}

/**
* Use this type to wrap your JSON-RPC service APIs to ensure you only use types that can be serialized through JSON-RPC.
*
* It replaces any occurence of `undefined` in the signature with `undefined | null` because when passing `undefined` it
* becomes `null` when serialized in JSON. The implementations must be able to handle both cases.
*/
export type JsonRpcService<T extends JsonRpcMethods> = {
[K in keyof T]: (...args: UndefinedOrNull<Parameters<T[K]>>) => Promise<UndefinedOrNull<Unpromise<ReturnType<T[K]>>>>
};
50 changes: 6 additions & 44 deletions packages/core/src/common/messaging/proxy-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,6 @@ export type JsonRpcServer<Client> = Disposable & {
getClient?(): Client | undefined;
};

/**
* Since neither JSON nor JSON-RPC handle undefined values in method argument arrays, we'll transform those
* into a structure of [argument_index, argument][] so that we can omit certain values and properly reconstruct
* the original array with undefined values. See `createJsonRpcParameters` and `parseJsonRpcParameters`.
* Note that any nested array will see its `undefined` values replaced by `null` because of the JSON serialization.
*/
export type JsonRpcParameters = [number, any][];

export interface JsonRpcConnectionEventEmitter {
readonly onDidOpenConnection: Event<void>;
readonly onDidCloseConnection: Event<void>;
Expand Down Expand Up @@ -136,23 +128,23 @@ export class JsonRpcProxyFactory<T extends object> implements ProxyHandler<T> {
listen(connection: MessageConnection): void {
// When a method is called without arguments then `params` is `undefined`.
// `token` seems to always be set by `vscode-jsonrpc` on the receiving side.
connection.onRequest((method: string, params: JsonRpcParameters | undefined, token: CancellationToken) => {
connection.onRequest((method: string, params: object | unknown[] | undefined, token: CancellationToken) => {
if (params === undefined) {
return this.onRequest(method, token);
}
if (!Array.isArray(params)) {
throw new Error('Unexpected JSON-RPC params format');
}
return this.onRequest(method, ...this.parseJsonRpcParameters(params), token);
return this.onRequest(method, ...params, token);
});
connection.onNotification((method: string, params: JsonRpcParameters | undefined) => {
connection.onNotification((method: string, params: object | unknown[] | undefined) => {
if (params === undefined) {
return this.onNotification(method);
}
if (!Array.isArray(params)) {
throw new Error('Unexpected JSON-RPC params format');
}
return this.onNotification(method, ...this.parseJsonRpcParameters(params));
return this.onNotification(method, ...params);
});
connection.onDispose(() => this.waitForConnection());
connection.onClose(() => {
Expand Down Expand Up @@ -261,23 +253,11 @@ export class JsonRpcProxyFactory<T extends object> implements ProxyHandler<T> {
const method = property.toString();
const capturedError = new Error(`Request '${method}' failed`);
const connection = await this.connectionPromise;
// Extract the cancellation token if it is the last argument passed to the method to call over JSON-RPC.
let token: CancellationToken | undefined;
if (CancellationToken.is(args[args.length - 1])) {
token = args.pop();
}
// Convert the list of arguments into a structure that allows us to conserve undefined values.
const params: any[] = this.createJsonRpcParameters(args);
// Push back the token if there was one.
// `vscode-jsonrpc` will remove the `CancellationToken` out of `params` when is is the last element.
if (token) {
params.push(token);
}
if (isNotify) {
connection.sendNotification(method, ParameterStructures.byPosition, ...params);
connection.sendNotification(method, ParameterStructures.byPosition, ...args);
} else {
try {
return await connection.sendRequest(method, ParameterStructures.byPosition, ...params);
return await connection.sendRequest(method, ParameterStructures.byPosition, ...args);
} catch (error) {
throw this.deserializeError(capturedError, error);
}
Expand Down Expand Up @@ -322,22 +302,4 @@ export class JsonRpcProxyFactory<T extends object> implements ProxyHandler<T> {
}
return e;
}

protected createJsonRpcParameters(args: any[]): JsonRpcParameters {
const params: JsonRpcParameters = [];
for (const [i, arg] of args.entries()) {
if (arg !== undefined) {
params.push([i, arg]);
}
}
return params;
}

protected parseJsonRpcParameters(params: JsonRpcParameters): any[] {
const args = [];
for (const [i, arg] of params) {
args[i] = arg;
}
return args;
}
}
2 changes: 2 additions & 0 deletions packages/core/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export type RecursivePartial<T> = {
};
export type MaybeArray<T> = T | T[];
export type MaybePromise<T> = T | PromiseLike<T>;
/** Extract `T` from `PromiseLike<T>`. */
export type Unpromise<T> = T extends PromiseLike<infer U> ? U : T;

export interface Prioritizeable<T> {
readonly priority: number;
Expand Down
8 changes: 7 additions & 1 deletion packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,13 @@ export class HostedPluginSupport {

protected async getStoragePath(): Promise<string | undefined> {
const roots = await this.workspaceService.roots;
return this.pluginPathsService.getHostStoragePath(this.workspaceService.workspace?.resource.toString(), roots.map(root => root.resource.toString()));
const storagePath = await this.pluginPathsService.getHostStoragePath(
this.workspaceService.workspace?.resource.toString(),
roots.map(root => root.resource.toString()),
);
if (storagePath) {
return storagePath;
}
}

protected async getHostGlobalStoragePath(): Promise<string> {
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-ext/src/main/browser/custom-editors/glob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse

if (!parsedPatterns.some(parsedPattern => (<ParsedExpressionPattern>parsedPattern).requiresSiblings!)) {
if (n === 1) {
// @ts-expect-error TS2322
return <ParsedStringPattern>parsedPatterns[0];
}

Expand Down Expand Up @@ -619,6 +620,7 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse
resultExpression.allPaths = allPaths;
}

// @ts-expect-error TS2322
return resultExpression;
}

Expand Down Expand Up @@ -656,6 +658,7 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse
resultExpression.allPaths = allPaths;
}

// @ts-expect-error TS2322
return resultExpression;
}

Expand Down
8 changes: 4 additions & 4 deletions packages/plugin-ext/src/main/browser/quick-open-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ export class QuickOpenMainImpl implements QuickOpenMain, Disposable {
quickPick.onDidChangeSelection((items: Array<monaco.quickInput.IQuickPickItem>) => {
this.proxy.$onDidChangeSelection(sessionId, items.map(item => (item as TransferQuickPickItems).handle));
});
quickPick.onDidTriggerButton((button: QuickInputButtonHandle) => {
this.proxy.$acceptOnDidTriggerButton(sessionId, button);
quickPick.onDidTriggerButton(button => {
this.proxy.$acceptOnDidTriggerButton(sessionId, button as QuickInputButtonHandle);
});
quickPick.onDidChangeValue((value: string) => {
this.proxy.$acceptDidChangeValue(sessionId, value);
Expand All @@ -227,8 +227,8 @@ export class QuickOpenMainImpl implements QuickOpenMain, Disposable {
inputBox.onDidAccept(() => {
this.proxy.$acceptOnDidAccept(sessionId);
});
inputBox.onDidTriggerButton((button: QuickInputButtonHandle) => {
this.proxy.$acceptOnDidTriggerButton(sessionId, button);
inputBox.onDidTriggerButton(button => {
this.proxy.$acceptOnDidTriggerButton(sessionId, button as QuickInputButtonHandle);
});
inputBox.onDidChangeValue((value: string) => {
this.proxy.$acceptDidChangeValue(sessionId, value);
Expand Down
6 changes: 4 additions & 2 deletions packages/plugin-ext/src/main/common/plugin-paths-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import type { JsonRpcService } from '@theia/core/lib/common/messaging/json-rpc-service';

export const pluginPathsServicePath = '/services/plugin-paths';

// Service to create plugin configuration folders for different purpose.
export const PluginPathsService = Symbol('PluginPathsService');
export interface PluginPathsService {
export type PluginPathsService = JsonRpcService<{
/** Returns hosted log path. Create directory by this path if it is not exist on the file system. */
getHostLogPath(): Promise<string>;
/** Returns storage path for given workspace */
getHostStoragePath(workspaceUri: string | undefined, rootUris: string[]): Promise<string | undefined>;
}
}>;
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class PluginPathsServiceImpl implements PluginPathsService {
return pluginDirPath;
}

async getHostStoragePath(workspaceUri: string | undefined, rootUris: string[]): Promise<string | undefined> {
async getHostStoragePath(workspaceUri: string | null | undefined, rootUris: string[]): Promise<string | undefined> {
const parentStorageDir = await this.getWorkspaceStorageDirPath();

if (!parentStorageDir) {
Expand Down
15 changes: 9 additions & 6 deletions packages/plugin-ext/src/main/node/plugin-server-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,19 @@ export class PluginServerHandler implements PluginServer {
return this.pluginDeployer.undeploy(pluginId);
}

setStorageValue(key: string, value: KeysToAnyValues, kind: PluginStorageKind): Promise<boolean> {
return this.pluginsKeyValueStorage.set(key, value, kind);
setStorageValue(key: string, value: KeysToAnyValues, kind: PluginStorageKind | CancellationToken): Promise<boolean> {
return this.pluginsKeyValueStorage.set(key, value, this.ignoreCancellationToken(kind));
}

getStorageValue(key: string, kind: PluginStorageKind): Promise<KeysToAnyValues> {
return this.pluginsKeyValueStorage.get(key, kind);
getStorageValue(key: string, kind: PluginStorageKind | CancellationToken): Promise<KeysToAnyValues> {
return this.pluginsKeyValueStorage.get(key, this.ignoreCancellationToken(kind));
}

getAllStorageValues(kind: PluginStorageKind): Promise<KeysToKeysToAnyValue> {
return this.pluginsKeyValueStorage.getAll(kind);
getAllStorageValues(kind: PluginStorageKind | CancellationToken): Promise<KeysToKeysToAnyValue> {
return this.pluginsKeyValueStorage.getAll(this.ignoreCancellationToken(kind));
}

protected ignoreCancellationToken<T>(what: T | CancellationToken): T | undefined {
return CancellationToken.Is(what) ? undefined : what;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ export class PluginsKeyValueStorage {
return this.deferredGlobalDataPath.promise;
}
const storagePath = await this.pluginPathsService.getHostStoragePath(kind.workspace, kind.roots);
return storagePath ? path.join(storagePath, 'workspace-state.json') : undefined;
if (storagePath) {
return path.join(storagePath, 'workspace-state.json');
}
}

private async readFromFile(pathToFile: string): Promise<KeysToKeysToAnyValue> {
Expand Down
26 changes: 21 additions & 5 deletions packages/plugin-ext/src/plugin/authentication-ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,26 @@ export class AuthenticationExtImpl implements AuthenticationExt {
return Object.freeze(this._providers.slice());
}

async getSession(requestingExtension: InternalPlugin, providerId: string, scopes: string[],
options: theia.AuthenticationGetSessionOptions & { createIfNone: true }): Promise<theia.AuthenticationSession>;
async getSession(requestingExtension: InternalPlugin, providerId: string, scopes: string[],
options: theia.AuthenticationGetSessionOptions = {}): Promise<theia.AuthenticationSession | undefined> {
async getSession(
requestingExtension: InternalPlugin,
providerId: string,
scopes: string[],
options?: theia.AuthenticationGetSessionOptions & { createIfNone: true }
): Promise<theia.AuthenticationSession>;

async getSession(
requestingExtension: InternalPlugin,
providerId: string,
scopes: string[],
options?: theia.AuthenticationGetSessionOptions
): Promise<theia.AuthenticationSession | undefined>;

async getSession(
requestingExtension: InternalPlugin,
providerId: string,
scopes: string[],
options: theia.AuthenticationGetSessionOptions & { createIfNone?: boolean } = {}
): Promise<theia.AuthenticationSession | undefined> {
const extensionName = requestingExtension.model.displayName || requestingExtension.model.name;
const extensionId = requestingExtension.model.id.toLowerCase();

Expand Down Expand Up @@ -163,7 +179,7 @@ export class AuthenticationExtImpl implements AuthenticationExt {
return Promise.resolve();
}

async $onDidChangeAuthenticationProviders(added: theia.AuthenticationProviderInformation[], removed: theia.AuthenticationProviderInformation[]): Promise<void> {
async $onDidChangeAuthenticationProviders(added: theia.AuthenticationProviderInformation[], removed: theia.AuthenticationProviderInformation[]): Promise<void> {
added.forEach(id => {
if (this._providers.indexOf(id) === -1) {
this._providers.push(id);
Expand Down
3 changes: 1 addition & 2 deletions packages/plugin-ext/src/plugin/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,7 @@ export class CommandsConverter {
// we're deprecating Command.id, so it has to be optional.
// Existing code will have compiled against a non - optional version of the field, so asserting it to exist is ok
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return KnownCommands.map((external.command || external.id)!, external.arguments, (mappedId: string, mappedArgs: any[]) =>
({
return KnownCommands.map((external.command || external.id)!, external.arguments, (mappedId: string, mappedArgs?: any[]) => ({
id: mappedId,
title: external.title || external.label || ' ',
tooltip: external.tooltip,
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-ext/src/plugin/known-commands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('Known Command Conversions', () => {

// when
// eslint-disable-next-line @typescript-eslint/no-explicit-any
KnownCommands.map(commandID, [uri, position], (mappedID: string, mappedArgs: any[]) => {
KnownCommands.map(commandID, [uri, position], (mappedID: string, mappedArgs: any[] = []) => {

// then
assert.strictEqual(commandID, mappedID);
Expand Down
21 changes: 13 additions & 8 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import type * as theia from '@theia/plugin';
import { CommandRegistryImpl } from './command-registry';
import { Emitter } from '@theia/core/lib/common/event';
import { CancellationError, CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import { CancellationError, CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import { QuickOpenExtImpl } from './quick-open';
import {
MAIN_RPC_CONTEXT,
Expand Down Expand Up @@ -229,8 +229,8 @@ export function createAPIFactory(
get providers(): ReadonlyArray<theia.AuthenticationProviderInformation> {
return authenticationExt.providers;
},
getSession(providerId: string, scopes: string[], options: theia.AuthenticationGetSessionOptions) {
return authenticationExt.getSession(plugin, providerId, scopes, options as any);
getSession(providerId: string, scopes: string[], options: theia.AuthenticationGetSessionOptions = {}): Promise<theia.AuthenticationSession> {
return authenticationExt.getSession(plugin, providerId, scopes, { ...options, createIfNone: true });
},
logout(providerId: string, sessionId: string): Thenable<void> {
return authenticationExt.logout(providerId, sessionId);
Expand Down Expand Up @@ -530,13 +530,18 @@ export function createAPIFactory(
const data = await documents.openDocument(uri);
return data && data.document;
},
createFileSystemWatcher: (pattern, ignoreCreate, ignoreChange, ignoreDelete): theia.FileSystemWatcher =>
extHostFileSystemEvent.createFileSystemWatcher(fromGlobPattern(pattern), ignoreCreate, ignoreChange, ignoreDelete),
findFiles(include: theia.GlobPattern, exclude?: theia.GlobPattern | null, maxResults?: number, token?: CancellationToken): PromiseLike<URI[]> {
createFileSystemWatcher(pattern, ignoreCreate, ignoreChange, ignoreDelete): theia.FileSystemWatcher {
return extHostFileSystemEvent.createFileSystemWatcher(fromGlobPattern(pattern), ignoreCreate, ignoreChange, ignoreDelete);
},
findFiles(include: theia.GlobPattern, exclude?: theia.GlobPattern | null, maxResults?: number, token?: theia.CancellationToken): PromiseLike<URI[]> {
return workspaceExt.findFiles(include, exclude, maxResults, token);
},
findTextInFiles(query: theia.TextSearchQuery, optionsOrCallback: theia.FindTextInFilesOptions | ((result: theia.TextSearchResult) => void),
callbackOrToken?: CancellationToken | ((result: theia.TextSearchResult) => void), token?: CancellationToken): Promise<theia.TextSearchComplete> {
findTextInFiles(
query: theia.TextSearchQuery,
optionsOrCallback: theia.FindTextInFilesOptions | ((result: theia.TextSearchResult) => void),
callbackOrToken?: theia.CancellationToken | ((result: theia.TextSearchResult) => void),
token?: theia.CancellationToken
): Promise<theia.TextSearchComplete> {
return workspaceExt.findTextInFiles(query, optionsOrCallback, callbackOrToken, token);
},
saveAll(includeUntitled?: boolean): PromiseLike<boolean> {
Expand Down
Loading

0 comments on commit e179450

Please sign in to comment.