@ikaru5/heimdall-controller
Version:
Structure your REST requests by communicating with contracts.
120 lines (107 loc) • 4.01 kB
JavaScript
// 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