UNPKG

event-driven

Version:

makes building event-driven-architecture easy

130 lines (116 loc) 3.85 kB
import { JetStreamClient, JsMsg, StringCodec } from 'nats' import { Types } from 'mongoose' import { Event, getFail, __NO_ACK__ } from '../backend' import { durableNames } from '..' export interface EventConfig { clientGroup: string js: JetStreamClient parentIds: Types.ObjectId[] durableName?: string } export interface SubscriberConfig { clientGroup: string js: JetStreamClient batch: number expires: number } const sc = StringCodec() interface ConvertedMsg<ParentEvent extends Event<string>> { jsMsg: JsMsg parentId: Types.ObjectId msgData: ParentEvent['data'] retryingDurableName: string | undefined // fail: (error: Error) => Promise<void> } export const getDurableName = (clientGroup: string, subject: string, i: number, type: 'PULL' | 'GROUP') => { const durableName = `${clientGroup}=${subject.replace(/\./g, '-')}=${type}=${i}` const exists = durableNames.find((name) => name === durableName) if (exists) throw new Error(`Duplicate Subscription on same clientGroup(${clientGroup}) & subject(${subject}) & type(${type}) pair`) else durableNames.push(durableName) return durableName } export const convertJsMsg = <ParentEvent extends Event<string>>(jsMsg: JsMsg): ConvertedMsg<ParentEvent> => { jsMsg.info const headers = jsMsg.headers const eventId = headers?.get('eventId') const parentId = new Types.ObjectId(eventId) const msgData = JSON.parse(sc.decode(jsMsg.data)) as ParentEvent['data'] const retryingDurableName = headers?.get('retryingDurableName') return { jsMsg, parentId, msgData, retryingDurableName } } export const filterConvertedMsgs = ( clientGroup: string, subject: string, eventHandlerOrder: number, showError: boolean, durableName: string, maxRetryCount: number ) => <ParentEvent extends Event<string>>(args: ConvertedMsg<ParentEvent>) => { if (args.retryingDurableName && args.retryingDurableName !== durableName) { args.jsMsg.ack() return false } else if (args.jsMsg.info.redeliveryCount > maxRetryCount) { getFail({ ack: () => args.jsMsg.ack(), nak: (delay: number) => args.jsMsg.nak(delay), parentIds: [args.parentId], durableName, clientGroup, subject, eventHandlerOrder, maxRetryCount, showError, })(new Error(__NO_ACK__)) return false } else return true } export const groupByFn = <T, K extends keyof any>(list: T[], getKey: (item: T) => K) => Object.values<T[]>( list.reduce((previous, currentItem) => { const group = getKey(currentItem) if (!previous[group]) previous[group] = [] previous[group].push(currentItem) return previous }, {} as Record<K, T[]>) ) export interface SubscriberEvent<ParentEvent extends Event<string>> { input: ParentEvent['data'] ack: () => void fail: (error: Error) => Promise<void> config: EventConfig } export interface SubscriberGroupedEvent<ParentEvent extends Event<string>> { inputs: ParentEvent['data'][] ack: () => void fail: (error: Error) => Promise<void> config: EventConfig } export const autoAckOrFail = <T extends SubscriberEvent<Event<string>>>(fn: (args: { config: EventConfig; input: T['input'] }) => any) => (events: T[]) => Promise.all( events.map(async (event) => { try { await fn(event) event.ack() } catch (err) { event.fail(err as Error) } }) ) export const autoGroupAckOrFail = <T extends SubscriberGroupedEvent<Event<string>>>(fn: (args: { config: EventConfig; inputs: T['inputs'] }) => any) => (events: T[]) => Promise.all( events.map(async (event) => { try { await fn(event) event.ack() } catch (err) { event.fail(err as Error) } }) )