✨ ooconnector: callback command retreival system (#525)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import axios from 'axios';
|
||||
import { ONLY_OFFICE_SERVER, onlyOfficeConnectivityCheckPeriodMS } from '@config';
|
||||
import { ONLY_OFFICE_SERVER, onlyOfficeConnectivityCheckPeriodMS, onlyOfficeCallbackTimeoutMS } from '@config';
|
||||
import { PolledThingieValue } from '@/lib/polled-thingie-value';
|
||||
import { PendingRequestQueue, PendingRequestCallback } from '@/lib/pending-request-matcher';
|
||||
import logger from '@/lib/logger';
|
||||
import * as Utils from '@/utils';
|
||||
|
||||
@@ -14,6 +16,12 @@ export enum ErrorCode {
|
||||
COMMAND_NOT_CORRECT = 5,
|
||||
INVALID_TOKEN = 6,
|
||||
}
|
||||
|
||||
/** Error generated by this connector. Should be string to share field with {@link ErrorCode} */
|
||||
export enum ConnectorErrorCode {
|
||||
CALLBACK_TIMEOUT = 'callback_timeout',
|
||||
}
|
||||
|
||||
/** Return the name of the error code in the `ErrorCode` enum if recognised, or a descript string */
|
||||
export const ErrorCodeFromValue = (value: number) => Utils.getKeyForValueSafe(value, ErrorCode, 'OnlyOffice.ErrorCode');
|
||||
|
||||
@@ -59,6 +67,7 @@ export namespace Callback {
|
||||
url?: string;
|
||||
actions?: Action[];
|
||||
users?: string[];
|
||||
userdata?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +102,7 @@ namespace CommandService {
|
||||
async postUnsafe(): Promise<ErrorResponse | TSuccessResponse> {
|
||||
logger.silly(`OnlyOffice command ${this.c} sent: ${JSON.stringify(this)}`);
|
||||
const result = await axios.post(Utils.joinURL([ONLY_OFFICE_SERVER, 'coauthoring/CommandService.ashx']), this);
|
||||
logger.info(`OnlyOffice command ${this.c} response: ${result.status}: ${JSON.stringify(result.data)}`);
|
||||
logger.info(`OnlyOffice command ${this.c} response ${result.status}: ${ErrorCodeFromValue(result.data.error)}: ${JSON.stringify(result.data)}`);
|
||||
return result.data as ErrorResponse | TSuccessResponse;
|
||||
}
|
||||
|
||||
@@ -174,6 +183,32 @@ namespace CommandService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Info {
|
||||
export type Response = SuccessResponse;
|
||||
export class Request extends BaseRequest<Response> {
|
||||
constructor(public readonly key: string, public readonly userdata: string = '') {
|
||||
super('info');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This object holds possible outcomes for commands like `info` who's result
|
||||
* is sent to the callback instead of replied to the request
|
||||
*/
|
||||
class CallbackResponseFromCommand {
|
||||
/**
|
||||
* If the `info` command returned an error code, this is it, or it can
|
||||
* be an internal error to this connector (eg.: timeout waiting for callback).
|
||||
* (If the `info` command returned {@link ErrorCode.SUCCESS} then this field will be `undefined`)
|
||||
*/
|
||||
public readonly error: Exclude<ErrorCode, ErrorCode.SUCCESS> | ConnectorErrorCode | undefined;
|
||||
|
||||
public constructor(error: ErrorCode | ConnectorErrorCode, public readonly result?: Callback.Parameters) {
|
||||
this.error = error === ErrorCode.SUCCESS ? undefined : error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,6 +217,8 @@ namespace CommandService {
|
||||
*/
|
||||
class OnlyOfficeService {
|
||||
private readonly poller: PolledThingieValue<CommandService.License.Response>;
|
||||
// Technically the timeout field is from the PendingRequestQueue but avoid 2 classes
|
||||
private readonly pendingRequests = new PendingRequestQueue<CallbackResponseFromCommand>(onlyOfficeCallbackTimeoutMS);
|
||||
|
||||
constructor() {
|
||||
this.poller = new PolledThingieValue(
|
||||
@@ -227,6 +264,20 @@ class OnlyOfficeService {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/** Generates and returns a random UUID that has a matching pending task enqueued for */
|
||||
private enqueuePendingCallback(key: string, callback: PendingRequestCallback<CallbackResponseFromCommand>): string {
|
||||
const userdata = randomUUID();
|
||||
this.pendingRequests.enqueue(key, userdata, callback);
|
||||
return userdata;
|
||||
}
|
||||
|
||||
/** Called by the OnlyOffice controller when the OO document editing services uses our callback */
|
||||
async ooCallbackCalled(result: Callback.Parameters) {
|
||||
if (!result.userdata) return;
|
||||
logger.info('OO Callback pending request response received', result);
|
||||
return this.pendingRequests.gotResult(result.key, result.userdata, new CallbackResponseFromCommand(ErrorCode.SUCCESS, result));
|
||||
}
|
||||
|
||||
// Note that `async` is important in the functions below. While they avoid the overhead
|
||||
// of `await`, the `async` is still required to catch the throw in `.post()`
|
||||
|
||||
@@ -239,6 +290,40 @@ class OnlyOfficeService {
|
||||
//TODO: When typing the response more fully, don't return the response object itself as here
|
||||
return new CommandService.License.Request().post();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a document status and the list of the identifiers of the users who opened the document for editing.
|
||||
* The response will be sent to the callback handler.
|
||||
* This method just sends the command. The response from the callback will be ignored if
|
||||
* this called by itself
|
||||
*
|
||||
* *Warning*: returns non succesful error codes instead of throwing errors like the other
|
||||
* methods. This is because the immediate response is likely to be itself usefull
|
||||
* for detecting errors.
|
||||
*/
|
||||
async getInfoUnsafe(key: string, userdata: string): Promise<ErrorCode> {
|
||||
return await new CommandService.Info.Request(key, userdata).postUnsafe().then(({ error }) => error);
|
||||
}
|
||||
|
||||
async getInfoAndWaitForCallbackUnsafe(key: string): Promise<CallbackResponseFromCommand> {
|
||||
// const userdata = randomUUID();
|
||||
return new Promise((resolve, reject) => {
|
||||
const userdata = this.enqueuePendingCallback(key, async (timeout, result) => {
|
||||
// The callback has called, unless timeout = true, or result is undefined (cancelled request)
|
||||
return resolve(timeout ? new CallbackResponseFromCommand(ConnectorErrorCode.CALLBACK_TIMEOUT) : result);
|
||||
});
|
||||
void this.getInfoUnsafe(key, userdata).then(
|
||||
response => {
|
||||
// The command service responded to the `info` command request
|
||||
if (response !== ErrorCode.SUCCESS) return this.pendingRequests.gotResult(key, userdata, new CallbackResponseFromCommand(response));
|
||||
// If it succeded, just wait for timeout or resolution by the callback
|
||||
},
|
||||
error => {
|
||||
this.pendingRequests.cancelPending(key, userdata).then(() => reject(error));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
/** Force a save in the editing session key provided. `userdata` will be forwarded to the callback */
|
||||
async forceSave(key: string, userdata = ''): Promise<string> {
|
||||
return new CommandService.ForceSave.Request(key, userdata).post().then(response => response.key);
|
||||
|
||||
Reference in New Issue
Block a user