@cap-js-community/event-queue
Version:
An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.
163 lines (142 loc) • 5.14 kB
JavaScript
;
const cds = require("@sap/cds");
const { publishEvent } = require("../publishEvent");
const config = require("../config");
const OUTBOXED = Symbol("outboxed");
const UNBOXED = Symbol("unboxed");
const CDS_EVENT_TYPE = "CAP_OUTBOX";
const COMPONENT_NAME = "/eventQueue/eventQueueAsOutbox";
const EVENT_QUEUE_SPECIFIC_FIELDS = ["startAfter", "referenceEntity", "referenceEntityKey", "namespace"];
const TO_COPY = ["inbound", "event", "data", "queue", "results", "method", "path", "params", "entity", "service"];
function outboxed(srv, customOpts) {
if (!(new.target || customOpts)) {
const former = srv[OUTBOXED];
if (former) {
return former;
}
}
const logger = cds.log(COMPONENT_NAME);
const originalSrv = srv[UNBOXED] || srv;
const outboxedSrv = Object.create(originalSrv);
outboxedSrv[UNBOXED] = originalSrv;
if (!new.target) {
if (!srv[OUTBOXED]) {
Object.defineProperty(srv, OUTBOXED, { value: outboxedSrv });
}
}
outboxedSrv.handle = async function (req) {
const context = req.context || cds.context;
const hasSpecificSettings = !!config.getCdsOutboxEventSpecificConfig(srv.name, req.event);
const subType = hasSpecificSettings ? [srv.name, req.event].join(".") : srv.name;
let srvConfig = config.findBaseCAPServiceWithoutNamespace(subType);
// NOTE: service is outboxed without config in cds.env.requires[srv]
if (!srvConfig) {
config.addCAPServiceWithoutEnvConfig(subType, srv, customOpts);
srvConfig = config.findBaseCAPServiceWithoutNamespace(subType);
}
const outboxOpts = config.getEventConfig(CDS_EVENT_TYPE, subType, srvConfig.namespace);
const eventHeaders = getPropagatedHeaders(outboxOpts, req);
const contextProperties = getPropagateContextProperties(outboxOpts, req);
if (["persistent-outbox", "persistent-queue"].includes(outboxOpts.kind)) {
await _mapToEventAndPublish(req, srvConfig.namespace, subType, eventHeaders, contextProperties);
return;
}
context.on("succeeded", async () => {
try {
if (req.reply) {
await originalSrv.send(req);
} else {
await originalSrv.emit(req);
}
} catch (err) {
logger.error("In memory processing failed", { event: req.event, cause: err });
if (isUnrecoverable(originalSrv, err) && outboxOpts.crashOnError !== false) {
cds.exit(1);
}
}
});
};
return outboxedSrv;
}
function unboxed(srv) {
return srv[UNBOXED] || srv;
}
const getPropagatedHeaders = (config, req) => {
const propagateHeaders = config.propagateHeaders.reduce((headers, headerName) => {
if (headerName in req.tx.context.headers) {
headers[headerName] = req.tx.context.headers[headerName];
}
return headers;
}, {});
return Object.assign(propagateHeaders, req.headers);
};
const getPropagateContextProperties = (config, req) => {
return config.propagateContextProperties.reduce((properties, name) => {
if (name in req.tx.context) {
properties[name] = req.tx.context[name];
}
return properties;
}, {});
};
const _mapToEventAndPublish = async (req, namespace, subType, eventHeaders, contextProperties) => {
const context = req.context || cds.context;
const eventQueueSpecificValues = {};
for (const header in req.headers ?? {}) {
for (const field of EVENT_QUEUE_SPECIFIC_FIELDS) {
if (header.toLocaleLowerCase() === `x-eventqueue-${field.toLocaleLowerCase()}`) {
eventQueueSpecificValues[field] = req.headers[header];
delete eventHeaders[header];
break;
}
}
}
const event = {
contextUser: context.user.id,
...(req._fromSend || (req.reply && { _fromSend: true })), // send or emit
...(eventHeaders && { headers: eventHeaders }),
...(Object.keys(contextProperties).length && { ...contextProperties }),
};
for (const prop of TO_COPY) {
if (req[prop]) {
event[prop] = req[prop];
}
}
if (req.query) {
event.query = typeof req.query.flat === "function" ? req.query.flat() : req.query;
delete event.query._target;
delete event.query.__target;
delete event.query.target;
delete event.data; // `req.data` should be a getter to whatever is in `req.query`
}
await publishEvent(
cds.tx(context),
{
type: CDS_EVENT_TYPE,
subType,
payload: JSON.stringify(event),
namespace: eventQueueSpecificValues.namespace ?? namespace,
...eventQueueSpecificValues,
},
{ allowNotExistingConfiguration: !!eventQueueSpecificValues.namespace }
);
};
const isUnrecoverable = (service, error) => {
let unrecoverable = service.isUnrecoverableError && service.isUnrecoverableError(error);
if (unrecoverable === undefined) {
unrecoverable = error.unrecoverable;
}
return unrecoverable || isStandardError(error);
};
const isStandardError = (err) => {
return (
err instanceof TypeError ||
err instanceof ReferenceError ||
err instanceof SyntaxError ||
err instanceof RangeError ||
err instanceof URIError
);
};
module.exports = {
outboxed,
unboxed,
};