UNPKG

@ikaru5/heimdall-controller

Version:

Structure your REST requests by communicating with contracts.

120 lines (107 loc) 4.01 kB
// bumped when the envelope shape changes - lets backends adapt gracefully export const PROTOCOL_VERSION = 1 /** * A Package for the payload. Heimdall sets its own meta information in required fields. * Everything in the convention over configuration manner. :) */ class Package { // Bound by index.js after the singleton exists - a static reference instead // of importing index.js back, which would create a circular ESM import. static api = undefined /** * Payload needs to be at least an empty object to be transformed to JSON successfully. * Also set isReceiving to undefined, since we dont know if its an sending or receiving package. */ constructor() { this.payload = {} this.isReceiving = undefined } /** * Returns an instance of package for sending out. * @param payload * @param {Object} options * supported options: receiver, path, port, host, protocol, decorate, csrfToken * @returns {Package} */ static buildSend(payload, { receiver, files, port, host, protocol, path, csrfToken, decorate }) { let newPackage = new Package newPackage.payload = "function" === typeof payload.toObject ? payload.toObject() : payload newPackage.receiver = receiver newPackage.messageId = Package.generateMessageId() newPackage.files = files newPackage.path = path || Package.api.path newPackage.port = port || Package.api.port newPackage.host = host || Package.api.host newPackage.csrfToken = csrfToken || Package.api.csrfToken newPackage.protocol = protocol || Package.api.protocol newPackage.isReceiving = false decorate && newPackage._decorateSender() return newPackage } /** * Returns an instance of package which was received. * @param rawData * @param {Object} options * supported options: protocol * @returns {Package} */ static buildReceive(rawData, options = {}) { let newPackage = new Package newPackage.protocol = options.protocol || Package.api.protocol newPackage.isReceiving = true if ("object" === typeof rawData) { newPackage.receiver = rawData.receiver newPackage.csrfToken = rawData.csrf newPackage.payload = rawData.payload // present on responses to an own command, absent on server-initiated pushes newPackage.correlatesTo = rawData.correlatesTo } else { console.error(`Got invalid JSON: ${rawData}`) } return newPackage } /** * Transform to JSON and send it to backend through Heimdall. * @returns {boolean} */ sendOut() { if (this.isReceiving) { console.error("Trying to send a received package!") return false } Package.api._sendOut(this._buildJSON(), this.protocol, this.host, this.path, this.port, this.files, { messageId: this.messageId, receiver: this.receiver }) } /** * Client-generated identifier: backends echo it as correlatesTo on every * response caused by this package, apps use it to track in-flight commands. * @returns {String} */ static generateMessageId() { if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID() return `mid-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}` } /** * Build the JSON String of Package. * @returns {String} * @private */ _buildJSON() { let out = { version: PROTOCOL_VERSION, messageId: this.messageId, payload: this.payload } if (undefined !== this.receiver) out["receiver"] = this.receiver if (undefined !== this.csrfToken && Package.api.handleCSRF) out["csrfToken"] = this.csrfToken return JSON.stringify(out) } /** * Decorates the sender package to make it syntactically beautiful for your backend. (For exp.: Ruby or Python styled receiver) * @private */ _decorateSender() { if (undefined !== this.receiver && undefined !== Package.api.decorateForReceiver) { this.receiver = Package.api.decorateForReceiver(this.receiver) } } } export default Package