UNPKG

@atomicjolt/lti-client

Version:

Client Javascript libraries to handle LTI.

154 lines (131 loc) 4.72 kB
import { PostMessageError, PostMessageErrorType } from "./error"; import { PostMessageCapabilitiesResponse, PostMessageCapability, PostMessageRequest, PostMessageResponse, } from "./types"; export interface PostMessageClientOptions { origin: string; targetFrame?: Window | string | null; timeout?: number; } const DEFAULT_OPTIONS: PostMessageClientOptions = { origin: "*", targetFrame: null, timeout: 2000, }; /** * A client for sending and receiving messages via the postMessage API according the the LTI postMessage specification * https://www.imsglobal.org/spec/lti-cs-pm/v0p1#response-parameters */ export class PostMessageClient { defaultOptions: Partial<PostMessageClientOptions>; constructor(options?: Partial<PostMessageClientOptions>) { this.defaultOptions = { ...DEFAULT_OPTIONS, ...options }; } /** Send a request to the LTI platform via the postMessage API and recieve back the platforms response * If the request times out, a PostMessageError with type Timeout will be thrown * If the platform returns an error, a PostMessageError with type ResponseError will be thrown */ public async send< Request extends PostMessageRequest = PostMessageRequest, Response extends PostMessageResponse = PostMessageResponse< Request["subject"] > >( payload: Request, options: Partial<PostMessageClientOptions> = {} ): Promise<Response> { const allOptions = { ...this.defaultOptions, ...options, }; const frame = await this.findTargetFrame( payload.subject, allOptions.targetFrame ?? null ); return new Promise<Response>((resolve, reject) => { const timeout = setTimeout(() => { reject(new PostMessageError(PostMessageErrorType.Timeout, payload)); }, allOptions.timeout); const receiveMessage = (event: MessageEvent<Response>) => { if ( typeof event.data === "object" && event.data.subject === `${payload.subject}.response` && event.data.message_id === payload.message_id && (event.origin === allOptions.origin || (allOptions.origin === "*" && event.origin !== "null")) ) { window.removeEventListener("message", receiveMessage); clearTimeout(timeout); if (event.data.error) { reject( new PostMessageError( PostMessageErrorType.ResponseError, payload, event.data ) ); } else { resolve(event.data); } } }; window.addEventListener("message", receiveMessage); frame.postMessage(payload, { targetOrigin: allOptions.origin, }); }); } /** Retrieve the list of message capabilities that the platform supports */ public async getCapabilities(): Promise<PostMessageCapability[]> { const response = await this.send< PostMessageRequest, PostMessageCapabilitiesResponse >( { subject: "lti.capabilities", message_id: "lti-caps" }, { origin: "*", targetFrame: window.parent ?? window.opener } ); return response.supported_messages; } /** Gets the configuration for a capability if the platform supports it, null otherwise */ public async getCapability( capability: string ): Promise<PostMessageCapability | null> { const capabilities = await this.getCapabilities(); return capabilities.find((c: any) => c.subject === capability) ?? null; } /** Generate a unique message id for a request */ public messageId(subject: string, ...args: string[]): string { const random = Math.random().toString(36).substring(2); return `${subject}-${args.join("-")}-${random}`; } private async findTargetFrame( subject: string, target: Window | string | null ): Promise<Window> { if (typeof target !== "string" && target !== null) return target; if (target == null) { // The platform can provide all of the supported capabilities // so we need to check for the lti.get_data capability and that will // tell us the frame to talk to. const cap = await this.getCapability(subject); target = cap?.frame ?? "_parent"; } const parent = window.parent || window.opener; if (target === "_parent") { return parent; } else { return parent.frames[target as any] || parent; } } } export abstract class PostMessageClientWrapper { protected client: PostMessageClient; static MessageTypes: string[] | null = null; constructor(client?: PostMessageClient) { this.client = client ?? new PostMessageClient(); } abstract isSupported(): Promise<boolean>; }