(core) Converting server-side Comm.js to typescript

Summary:
- Add app/common/CommTypes.ts to define types shared by client and server.
- Include @types/ws npm package

Test Plan: Intended to have no changes in behavior

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3467
This commit is contained in:
Dmitry S
2022-06-04 00:12:30 -04:00
parent 519f1be93a
commit 4f1cb53b29
23 changed files with 655 additions and 716 deletions

View File

@@ -16,118 +16,26 @@
*
* Implementation
* --------------
* Messages are serialized as follows. Note that this is a matter between the client's and the
* server's communication libraries, and code outside of them should not rely on these details.
* Requests: {
* reqId: Number,
* method: String,
* args: Array
* }
* Responses: {
* reqId: Number, // distinguishes responses from async messages
* error: String // if the request failed
* data: Object // if the request succeeded, may be undefined if nothing to return
* }
* Async messages from server: {
* type: String, // 'docListAction' or 'docUserAction' or 'clientConnect'
* docFD: Number, // For 'docUserAction', the file descriptor of the open document.
* data: Object // The message data.
* // other keys may exist depending on message type.
* }
* Messages are serialized as JSON using types CommRequest, CommResponse, CommResponseError for
* method calls, and CommMessage for async messages from the server. These are all defined in
* app/common/CommTypes. Note that this is a matter between the client's and the server's
* communication libraries, and code outside of them should not rely on these details.
*/
import {GristWSConnection} from 'app/client/components/GristWSConnection';
import * as dispose from 'app/client/lib/dispose';
import {CommMessage, CommRequest, CommResponse, CommResponseError, ValidEvent} from 'app/common/CommTypes';
import {UserAction} from 'app/common/DocActions';
import {DocListAPI, OpenLocalDocResult} from 'app/common/DocListAPI';
import {GristServerAPI} from 'app/common/GristServerAPI';
import {StringUnion} from 'app/common/StringUnion';
import {getInitialDocAssignment} from 'app/common/urlUtils';
import {Events as BackboneEvents} from 'backbone';
// tslint:disable:no-console
/**
* Event for a change to the document list.
* These are sent to all connected clients, regardless of which documents they have open.
* TODO: implement and document.
* @event docListAction
*/
/**
* Event for a user action on a document, or part of one. Sent to all clients that have this
* document open.
* @event docUserAction
* @property {Number} docFD - The file descriptor of the open document, specific to each client.
* @property {Array} data.actionGroup - ActionGroup object containing user action, and doc actions.
* @property {Boolean} fromSelf - Flag to indicate whether the action originated from this client.
*/
/**
* Event for a change to document usage. Sent to all clients that have this document open.
* @event docUsage
* @property {Number} docFD - The file descriptor of the open document, specific to each client.
* @property {FilteredDocUsageSummary} data.docUsage - Document usage summary.
* @property {Product} data.product - Product that was used to compute `data.docUsage`
*/
/**
* Event for when a document is forcibly shutdown, and requires the client to re-open it.
* @event docShutdown
* @property {Number} docFD - The file descriptor of the open document, specific to each client.
*/
/**
* Event sent by server received when a client first connects.
* @event clientConnect
* @property {Number} clientId - The ID for the client, which may be reused if a client reconnects
* to reattach to its state on the server.
* @property {Number} missedMessages - Array of messages missed from the server.
* @property {Object} settings - Object containing server settings and features which
* should be used to initialize the client.
* @property {Object} profile - Object containing session profile information if the user
* is signed in, or null otherwise. See "clientLogin" message below for fields.
*/
/**
* Event sent by server to all clients in the session when the updated profile is retrieved.
* Does not necessarily contain all properties, may only include updated properties.
* Gets sent on login with all properties.
* @event profileFetch
* @property {String} email User email.
* @property {String} name User name,
* @property {String} imageUrl The url of the user's profile image.
*/
/**
* Event sent by server to all clients in the session when the user settings are updated.
* @event userSettings
* @property {Object} features - Object containing feature flags such as login, indicating
* which features are activated.
*/
/**
* Event sent by server to all clients in the session when a client logs out.
* @event clientLogout
*/
/**
* Event sent by server to all clients when an invite is received or for all invites received
* while away when a user logs in.
* @event receiveInvites
* @property {Number} data - An array of unread invites (see app/common/sharing).
*/
const ValidEvent = StringUnion('docListAction', 'docUserAction', 'docShutdown', 'docError',
'docUsage', 'clientConnect', 'clientLogout',
'profileFetch', 'userSettings', 'receiveInvites');
type ValidEvent = typeof ValidEvent.type;
/**
* A request that is currently being processed.
*/
export interface CommRequestInFlight {
resolve: (result: any) => void;
resolve: (result: unknown) => void;
reject: (err: Error) => void;
// clientId is non-null for those requests which should not be re-sent on reconnect if
// the clientId has changed; it is null when it's safe to re-send.
@@ -138,50 +46,10 @@ export interface CommRequestInFlight {
sent: boolean;
}
/**
* A request in the appropriate form for sending to the server.
*/
export interface CommRequest {
reqId: number;
method: string;
args: any[];
}
/**
* A regular, successful response from the server.
*/
export interface CommResponse {
reqId: number;
data: any;
error?: null; // TODO: keep until sure server never sets this on regular responses.
}
/**
* An exceptional response from the server when there is an error.
*/
export interface CommResponseError {
reqId: number;
error: string;
errorCode: string;
shouldFork?: boolean; // if set, the server suggests forking the document.
details?: any; // if set, error has extra details available. TODO - the treatment of
// details could do with some harmonisation between rest API and ws API,
// and between front-end and back-end types.
}
function isCommResponseError(msg: CommResponse | CommResponseError): msg is CommResponseError {
return Boolean(msg.error);
}
/**
* A message pushed from the server, not in response to a request.
*/
export interface CommMessage {
type: ValidEvent;
docFD: number;
data: any;
}
/**
* Comm object provides the interfaces to communicate with the server.
* Each method that calls to the server returns a promise for the response.

View File

@@ -1,35 +1,14 @@
import {Comm, CommMessage} from 'app/client/components/Comm';
import {Comm} from 'app/client/components/Comm';
import {reportError, reportMessage} from 'app/client/models/errors';
import {Notifier} from 'app/client/models/NotifyModel';
import {ActionGroup} from 'app/common/ActionGroup';
import {ActiveDocAPI, ApplyUAOptions, ApplyUAResult} from 'app/common/ActiveDocAPI';
import {DocAction, UserAction} from 'app/common/DocActions';
import {CommMessage} from 'app/common/CommTypes';
import {UserAction} from 'app/common/DocActions';
import {OpenLocalDocResult} from 'app/common/DocListAPI';
import {FilteredDocUsageSummary} from 'app/common/DocUsage';
import {Product} from 'app/common/Features';
import {docUrl} from 'app/common/urlUtils';
import {Events as BackboneEvents} from 'backbone';
import {Disposable, Emitter} from 'grainjs';
// tslint:disable:no-console
export interface DocUserAction extends CommMessage {
fromSelf?: boolean;
data: {
docActions: DocAction[];
actionGroup: ActionGroup;
docUsage: FilteredDocUsageSummary;
error?: string;
};
}
export interface DocUsageMessage extends CommMessage {
data: {
docUsage: FilteredDocUsageSummary;
product?: Product;
};
}
const SLOW_NOTIFICATION_TIMEOUT_MS = 1000; // applies to user actions only
/**

View File

@@ -11,7 +11,7 @@ import {CodeEditorPanel} from 'app/client/components/CodeEditorPanel';
import * as commands from 'app/client/components/commands';
import {CursorPos} from 'app/client/components/Cursor';
import {CursorMonitor, ViewCursorPos} from "app/client/components/CursorMonitor";
import {DocComm, DocUsageMessage, DocUserAction} from 'app/client/components/DocComm';
import {DocComm} from 'app/client/components/DocComm';
import * as DocConfigTab from 'app/client/components/DocConfigTab';
import {Drafts} from "app/client/components/Drafts";
import {EditorMonitor} from "app/client/components/EditorMonitor";
@@ -51,6 +51,7 @@ import {FieldEditor} from "app/client/widgets/FieldEditor";
import {MinimalActionGroup} from 'app/common/ActionGroup';
import {ClientQuery} from "app/common/ActiveDocAPI";
import {delay} from 'app/common/delay';
import {CommDocUsage, CommDocUserAction} from 'app/common/CommTypes';
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
import {isSchemaAction, UserAction} from 'app/common/DocActions';
import {OpenLocalDocResult} from 'app/common/DocListAPI';
@@ -441,7 +442,7 @@ export class GristDoc extends DisposableWithEvents {
* Process actions received from the server by forwarding them to `docData.receiveAction()` and
* pushing them to actionLog.
*/
public onDocUserAction(message: DocUserAction) {
public onDocUserAction(message: CommDocUserAction) {
console.log("GristDoc.onDocUserAction", message);
let schemaUpdated = false;
/**
@@ -489,7 +490,7 @@ export class GristDoc extends DisposableWithEvents {
* Process usage and product received from the server by updating their respective
* observables.
*/
public onDocUsageMessage(message: DocUsageMessage) {
public onDocUsageMessage(message: CommDocUsage) {
if (!this.docComm.isActionFromThisDoc(message)) { return; }
bundleChanges(() => {

View File

@@ -15,6 +15,7 @@ import {createAppUI} from 'app/client/ui/AppUI';
import {addViewportTag} from 'app/client/ui/viewport';
import {attachCssRootVars} from 'app/client/ui2018/cssVars';
import {BaseAPI} from 'app/common/BaseAPI';
import {CommDocError} from 'app/common/CommTypes';
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
import {fetchFromHome} from 'app/common/urlUtils';
import {ISupportedFeatures} from 'app/common/UserConfig';
@@ -158,7 +159,7 @@ export class App extends DisposableWithEvents {
setTimeout(() => this.reloadPane(), 0);
});
this.listenTo(this.comm, 'docError', (msg) => {
this.listenTo(this.comm, 'docError', (msg: CommDocError) => {
this._checkError(new Error(msg.data.message));
});