@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.
140 lines (122 loc) • 4.34 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"];
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);
if (["persistent-outbox", "persistent-queue"].includes(outboxOpts.kind)) {
await _mapToEventAndPublish(context, subType, req, eventHeaders, srvConfig.namespace);
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 _mapToEventAndPublish = async (context, subType, req, eventHeaders, namespace) => {
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
...(req.inbound && { inbound: req.inbound }),
...(req.event && { event: req.event }),
...(req.data && { data: req.data }),
...(eventHeaders && { headers: eventHeaders }),
...(req.query && { query: 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,
};