UNPKG

@noggin/elastic-noggin-sdk

Version:
167 lines (144 loc) 6.27 kB
import { Observable, of } from "rxjs"; import { switchMap, delay, timeout, map, catchError, finalize } from "rxjs/operators"; import { Batch, Tip } from "./models/types"; import { EnoFactory } from "./EnoFactory"; import { IVars } from "./vars"; import { send } from "./send"; import { IEnSrvOptions } from "./IEnSrvOptions"; import { checkBatchForError } from "./error"; const DefaultRetryDelayMs = 5000; const DefaultRetryAttempts = 20; const DefaultTimeoutMs = 60000; const OpTimeoutBufferMs = 3000; // The millis subtracted from the timeout to leave enough time to check the status const MinOpTimeoutMs = 1000; // The minimum timeout in millis for the op/process const MaxOpTimeoutMs = 20000; // The maximum timeout in millis for the op/process const SessionInitTimeoutMs = 10000; export interface IProcessOptions { waitForFinish?: boolean; inputVars?: IVars; retryDelayMs?: number; retryAttempts?: number; timeoutMs?: number; } export interface IProcessResponse { operationTip: Tip; responseTip: Tip; isFinished: boolean; outputVars?: IVars; } // Starts a process on EnSrv // If waitForFinish, then the observable will not return until the process is finished. // Otherwise will return once the first response is received export function startProcess( tip: Tip, enSrvOptions: IEnSrvOptions, processOptions: IProcessOptions = {} ): Observable<IProcessResponse> { const enoFactory = new EnoFactory("op/process", "security/policy/op"); enoFactory.setField("op/process/process", [tip]); enoFactory.setField("op/process/inline-vars", [ JSON.stringify(processOptions.inputVars || {}), ]); const processOp = enoFactory.makeEno(); let attemptsRemaining = processOptions.retryAttempts || DefaultRetryAttempts; let checkResponse$: ( processResponse: IProcessResponse ) => Observable<IProcessResponse>; checkResponse$ = ( processResponse: IProcessResponse ): Observable<IProcessResponse> => { if (processResponse.isFinished || !processOptions.waitForFinish) { return of(processResponse); } if (attemptsRemaining-- > 0) { return getProcessStatus(processOp.tip, enSrvOptions).pipe( delay(processOptions.retryDelayMs || DefaultRetryDelayMs), switchMap(checkResponse$) ); } throw new Error("Too many attempts waiting for process to finish"); }; const timeoutMs = processOptions.timeoutMs || DefaultTimeoutMs; const opProcessTimeoutMs = Math.min(MaxOpTimeoutMs, Math.max(MinOpTimeoutMs, timeoutMs-OpTimeoutBufferMs)); // Check if we already have a session token const hasExistingSession = !!enSrvOptions.sessionToken; // Set maintainInitialSessionToken to true so that `send` populates the initialSessionToken const originalMaintainInitialSessionToken = enSrvOptions.maintainInitialSessionToken; enSrvOptions.maintainInitialSessionToken = true; // Create a function that returns the main process observable const createProcessObservable = () => send([processOp], enSrvOptions).pipe( // Timeout the op/process request slightly before the overall timeout to allow for checking the status timeout(opProcessTimeoutMs), // Convert the ENO batch to an IProcessResponse map((batch) => makeProcessResponseFromBatch(processOp.tip, batch)), // If we received any failure from starting the process or getting the process response out of the error // then proactively go fetch the status to double-check catchError(() => getProcessStatus(processOp.tip, enSrvOptions)), // Poll the status if its not finished and we need to wait for it to finish switchMap(checkResponse$), // Abort the whole thing if we timeout timeout(timeoutMs), // Revert maintainInitialSessionToken back to its original value after process completes finalize(() => { enSrvOptions.maintainInitialSessionToken = originalMaintainInitialSessionToken; }) ); // If we already have a session, skip session establishment and go directly to process if (hasExistingSession) { return createProcessObservable(); } // Otherwise, establish the session by sending an empty batch first return send([], enSrvOptions).pipe( timeout(SessionInitTimeoutMs), catchError(() => { // Session initialization failed - ignore the error and proceed to process // execution. The process execution could still succeed. However, if timeout // error occurs, the op/process/status will fail since we could not establish // an initial session. return of(null); }), // Send the process operation to EnSrv. The enSrvOptions is now populated with the sessionToken switchMap(() => createProcessObservable()) ); } // Get the status of a process operation export function getProcessStatus( processOpTip: Tip, enSrvOptions: IEnSrvOptions ): Observable<IProcessResponse> { // Create a copy of options and use initialSessionToken if available // 'op/process/status' requires the initial session the process was triggered with const statusOptions = { ...enSrvOptions }; if (statusOptions.initialSessionToken) { statusOptions.sessionToken = statusOptions.initialSessionToken; } const enoFactory = new EnoFactory("op/process/status", "security/policy/op"); enoFactory.setField("op/process/status:op-tip", [processOpTip]); const statusOpEno = enoFactory.makeEno(); return send([statusOpEno], statusOptions).pipe( map((batch) => makeProcessResponseFromBatch(processOpTip, batch)) ); } // Handle the process response batch, and retry if necessary function makeProcessResponseFromBatch( processOpTip: Tip, batch: Batch ): IProcessResponse { checkBatchForError(batch); for (let i = 0; i < batch.length; i++) { const eno = batch[i]; if ( eno.getType() === "response/process" && eno.getFieldStringValue("response/process/op-tip") === processOpTip ) { return { operationTip: processOpTip, responseTip: eno.tip, outputVars: eno.getFieldJsonValue("response/process/inline-vars"), isFinished: eno.getFieldBooleanValue("response/process/finished"), }; } } // Something went wrong. There was no response/process in the response batch throw new Error("error/message/server/internal"); }