2022-03-24 17:11:26 +00:00
|
|
|
import { DocAction } from 'app/common/DocActions';
|
2022-12-21 16:40:00 +00:00
|
|
|
import { DocData } from 'app/common/DocData';
|
|
|
|
import { SchemaTypes } from 'app/common/schema';
|
2022-03-24 17:11:26 +00:00
|
|
|
import { FlexServer } from 'app/server/lib/FlexServer';
|
|
|
|
import axios from 'axios';
|
|
|
|
import pick = require('lodash/pick');
|
2024-03-28 17:22:20 +00:00
|
|
|
import {GristClientSocket} from 'app/client/components/GristClientSocket';
|
2022-03-24 17:11:26 +00:00
|
|
|
|
|
|
|
interface GristRequest {
|
|
|
|
reqId: number;
|
|
|
|
method: string;
|
|
|
|
args: any[];
|
|
|
|
}
|
|
|
|
|
|
|
|
interface GristResponse {
|
|
|
|
reqId: number;
|
|
|
|
error?: string;
|
|
|
|
errorCode?: string;
|
|
|
|
data?: any;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface GristMessage {
|
|
|
|
type: 'clientConnect' | 'docUserAction';
|
|
|
|
docFD: number;
|
|
|
|
data: any;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class GristClient {
|
|
|
|
public messages: GristMessage[] = [];
|
|
|
|
|
|
|
|
private _requestId: number = 0;
|
|
|
|
private _pending: Array<GristResponse|GristMessage> = [];
|
2022-12-21 16:40:00 +00:00
|
|
|
private _docData?: DocData; // accumulate tabular info like a real client.
|
2022-03-24 17:11:26 +00:00
|
|
|
private _consumer: () => void;
|
|
|
|
private _ignoreTrivialActions: boolean = false;
|
|
|
|
|
2024-03-28 17:22:20 +00:00
|
|
|
constructor(public ws: GristClientSocket) {
|
|
|
|
ws.onmessage = (data: string) => {
|
|
|
|
const msg = pick(JSON.parse(data),
|
2022-03-24 17:11:26 +00:00
|
|
|
['reqId', 'error', 'errorCode', 'data', 'type', 'docFD']);
|
|
|
|
if (this._ignoreTrivialActions && msg.type === 'docUserAction' &&
|
|
|
|
msg.data?.actionGroup?.internal === true &&
|
|
|
|
msg.data?.docActions?.length === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._pending.push(msg);
|
2022-12-21 16:40:00 +00:00
|
|
|
if (msg.data?.doc) {
|
|
|
|
this._docData = new DocData(() => {
|
|
|
|
throw new Error('no fetches');
|
|
|
|
}, msg.data.doc);
|
|
|
|
}
|
|
|
|
if (this._docData && msg.type === 'docUserAction') {
|
|
|
|
const docActions = msg.data?.docActions || [];
|
|
|
|
for (const docAction of docActions) {
|
|
|
|
this._docData.receiveAction(docAction);
|
|
|
|
}
|
|
|
|
}
|
2022-03-24 17:11:26 +00:00
|
|
|
if (this._consumer) { this._consumer(); }
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// After a document is opened, the sandbox recomputes its formulas and sends any changes.
|
|
|
|
// The client will receive an update even if there are no changes. This may be useful in
|
|
|
|
// the future to know that the document is up to date. But for testing, this asynchronous
|
|
|
|
// message can be awkward. Call this method to ignore it.
|
|
|
|
public ignoreTrivialActions() {
|
|
|
|
this._ignoreTrivialActions = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public flush() {
|
|
|
|
this._pending = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
public shift() {
|
|
|
|
return this._pending.shift();
|
|
|
|
}
|
|
|
|
|
|
|
|
public count() {
|
|
|
|
return this._pending.length;
|
|
|
|
}
|
|
|
|
|
2022-12-21 16:40:00 +00:00
|
|
|
public get docData() {
|
|
|
|
if (!this._docData) { throw new Error('no DocData'); }
|
|
|
|
return this._docData;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getMetaRecords(tableId: keyof SchemaTypes) {
|
|
|
|
return this.docData.getMetaTable(tableId).getRecords();
|
|
|
|
}
|
|
|
|
|
2022-03-24 17:11:26 +00:00
|
|
|
public async read(): Promise<any> {
|
|
|
|
for (;;) {
|
|
|
|
if (this._pending.length) {
|
|
|
|
return this._pending.shift();
|
|
|
|
}
|
2022-03-23 13:41:34 +00:00
|
|
|
await new Promise<void>(resolve => this._consumer = resolve);
|
2022-03-24 17:11:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async readMessage(): Promise<GristMessage> {
|
|
|
|
const result = await this.read();
|
|
|
|
if (!result.type) {
|
|
|
|
throw new Error(`message looks wrong: ${JSON.stringify(result)}`);
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async readResponse(): Promise<GristResponse> {
|
|
|
|
this.messages = [];
|
|
|
|
for (;;) {
|
|
|
|
const result = await this.read();
|
|
|
|
if (result.reqId === undefined) {
|
|
|
|
this.messages.push(result);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (result.reqId !== this._requestId) {
|
|
|
|
throw new Error("unexpected request id");
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-21 16:40:00 +00:00
|
|
|
public waitForServer() {
|
|
|
|
// send an arbitrary failing message and wait for response.
|
|
|
|
return this.send('ping');
|
|
|
|
}
|
|
|
|
|
2022-03-24 17:11:26 +00:00
|
|
|
// Helper to read the next docUserAction ignoring anything else (e.g. a duplicate clientConnect).
|
|
|
|
public async readDocUserAction(): Promise<DocAction[]> {
|
|
|
|
while (true) { // eslint-disable-line no-constant-condition
|
|
|
|
const msg = await this.readMessage();
|
|
|
|
if (msg.type === 'docUserAction') {
|
|
|
|
return msg.data.docActions;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async send(method: string, ...args: any[]): Promise<GristResponse> {
|
|
|
|
const p = this.readResponse();
|
|
|
|
this._requestId++;
|
|
|
|
const req: GristRequest = {
|
|
|
|
reqId: this._requestId,
|
|
|
|
method,
|
|
|
|
args
|
|
|
|
};
|
|
|
|
this.ws.send(JSON.stringify(req));
|
|
|
|
const result = await p;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async close() {
|
|
|
|
this.ws.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
public async openDocOnConnect(docId: string) {
|
|
|
|
const msg = await this.readMessage();
|
|
|
|
if (msg.type !== 'clientConnect') { throw new Error('expected clientConnect'); }
|
|
|
|
const openDoc = await this.send('openDoc', docId);
|
|
|
|
if (openDoc.error) { throw new Error('error in openDocOnConnect'); }
|
|
|
|
return openDoc;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function openClient(server: FlexServer, email: string, org: string,
|
|
|
|
emailHeader?: string): Promise<GristClient> {
|
|
|
|
const headers: Record<string, string> = {};
|
|
|
|
if (!emailHeader) {
|
|
|
|
const resp = await axios.get(`${server.getOwnUrl()}/test/session`);
|
|
|
|
const cookie = resp.headers['set-cookie'][0];
|
|
|
|
if (email !== 'anon@getgrist.com') {
|
|
|
|
const cid = decodeURIComponent(cookie.split('=')[1].split(';')[0]);
|
|
|
|
const comm = server.getComm();
|
2022-06-04 04:12:30 +00:00
|
|
|
const sessionId = comm.getSessionIdFromCookie(cid)!;
|
2022-03-24 17:11:26 +00:00
|
|
|
const scopedSession = comm.getOrCreateSession(sessionId, {org});
|
|
|
|
const profile = { email, email_verified: true, name: "Someone" };
|
|
|
|
await scopedSession.updateUserProfile({} as any, profile);
|
|
|
|
}
|
|
|
|
headers.Cookie = cookie;
|
|
|
|
} else {
|
|
|
|
headers[emailHeader] = email;
|
|
|
|
}
|
2024-03-28 17:22:20 +00:00
|
|
|
const ws = new GristClientSocket('ws://localhost:' + server.getOwnPort() + `/o/${org}`, {
|
2022-03-24 17:11:26 +00:00
|
|
|
headers
|
|
|
|
});
|
2022-06-04 04:12:30 +00:00
|
|
|
const client = new GristClient(ws);
|
2022-03-24 17:11:26 +00:00
|
|
|
await new Promise(function(resolve, reject) {
|
2024-03-28 17:22:20 +00:00
|
|
|
ws.onopen = function() {
|
|
|
|
ws.onerror = null;
|
2022-03-24 17:11:26 +00:00
|
|
|
resolve(ws);
|
2024-03-28 17:22:20 +00:00
|
|
|
};
|
|
|
|
ws.onerror = function(err: Error) {
|
|
|
|
ws.onopen = null;
|
2022-03-24 17:11:26 +00:00
|
|
|
reject(err);
|
2024-03-28 17:22:20 +00:00
|
|
|
};
|
2022-03-24 17:11:26 +00:00
|
|
|
});
|
2022-06-04 04:12:30 +00:00
|
|
|
return client;
|
2022-03-24 17:11:26 +00:00
|
|
|
}
|