event-driven
Version:
makes building event-driven-architecture easy
146 lines (135 loc) • 4.52 kB
text/typescript
import { Event, getFail } from '../backend'
import { JsMsg, JetStreamClient, consumerOpts } from 'nats'
import { _maxRetryCount as maxRetryCount, _showError as showError, _showProcessTimeWarning as showProcessTimeWarning } from '../'
import { SubscriberConfig, SubscriberEvent, convertJsMsg, filterConvertedMsgs, getDurableName } from './common'
export type PulledEventHandler<ParentEvent extends Event<string>> = (events: SubscriberEvent<ParentEvent>[]) => Promise<any>
const getSubsDetails = <ParentEvent extends Event<string>>(
subscription:
| PulledEventHandler<ParentEvent>
| [
PulledEventHandler<ParentEvent>,
{
batch?: number | undefined
expires?: number | undefined
}
]
) => {
if (typeof subscription === 'function') {
const eventHandler = subscription
return { eventHandler, batchCustom: undefined, expiresCustom: undefined }
} else {
const [eventHandler, { batch: batchCustom, expires: expiresCustom }] = subscription
return { eventHandler, batchCustom, expiresCustom }
}
}
export const SubscriberPull = (config: SubscriberConfig) => {
return async <ParentEvent extends Event<string>>({
subject,
subscriptions,
}: {
subject: ParentEvent['subject']
subscriptions: (PulledEventHandler<ParentEvent> | [PulledEventHandler<ParentEvent>, { batch?: number; expires?: number }])[]
}) => {
for (let i = 0; i < subscriptions.length; i++) {
const subscription = subscriptions[i]
const { eventHandler, batchCustom, expiresCustom } = getSubsDetails(subscription)
const durableName = getDurableName(config.clientGroup, subject, i, 'PULL')
try {
let msgs: JsMsg[] = []
const opts = consumerOpts()
opts.ackWait(30000)
opts.manualAck()
opts.durable(durableName)
opts.maxAckPending(500)
const psub = await config.js.pullSubscribe(subject, opts)
;(async () => {
for await (const m of psub) msgs.push(m)
})()
setInterval(() => {
if (msgs.length > 0) {
processEvents({
msgs: msgs.splice(0, 10000),
maxRetryCount,
js: config.js,
clientGroup: config.clientGroup,
eventHandler,
durableName,
subject,
eventHandlerOrder: i,
showError,
showProcessTimeWarning,
})
}
}, 100)
const batch = batchCustom ?? config.batch
const expires = expiresCustom ?? config.expires
psub.pull({ batch, expires })
setInterval(() => psub.pull({ batch, expires }), expires)
} catch (err) {
throw err
}
}
}
}
interface ProcessEventsInput<ParentEvent extends Event<string>> {
msgs: JsMsg[]
maxRetryCount: number
js: JetStreamClient
clientGroup: string
eventHandler: PulledEventHandler<ParentEvent>
durableName: string
subject: string
eventHandlerOrder: number
showError: boolean
showProcessTimeWarning?: number
}
const processEvents = async <ParentEvent extends Event<string>>({
msgs,
maxRetryCount,
js,
clientGroup,
eventHandler,
durableName,
subject,
eventHandlerOrder,
showError,
showProcessTimeWarning,
}: ProcessEventsInput<ParentEvent>) => {
const msgFilter = filterConvertedMsgs(clientGroup, subject, eventHandlerOrder, showError, durableName, maxRetryCount)
const events = msgs
.map(convertJsMsg)
.filter(msgFilter)
.map(({ jsMsg, parentId, msgData }) => {
const parentIds = [parentId]
const ack = () => jsMsg.ack()
const nak = (delay: number) => jsMsg.nak(delay)
const fail = getFail({
parentIds,
ack,
nak,
durableName,
clientGroup,
subject,
eventHandlerOrder,
maxRetryCount,
showError,
})
return {
input: msgData,
ack,
fail,
config: { js, clientGroup, parentIds, durableName },
}
})
try {
let start = new Date()
await eventHandler(events)
let diff = new Date().getTime() - start.getTime()
//TODO sstore process time
if (showProcessTimeWarning && showProcessTimeWarning < diff)
console.log('\x1b[33m%s\x1b[0m', `[WARNING-TIME] ${diff / 1000}s, durableName: ${durableName}`)
} catch (error) {
//TODO store unhandled errors
console.error('\x1b[31m%s\x1b[0m', `UNHANDLED SUBSCRIPTION BATCH ERRROR(${durableName})`, error)
}
}