@codecompose/typed-pubsub
Version:
A type-safe pub/sub abstraction for Google Cloud and Firebase
1 lines • 8.76 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":["firebaseOnMessagePublished","onMessagePublished","firebaseOnMessagePublished","handlerOptions: HandlerOptions","onMessagePublished"],"sources":["../src/constants.ts","../src/utils/should-drop-event.ts","../src/handler.ts","../src/publisher.ts","../src/factory.ts"],"sourcesContent":["export const HOUR_MINUTES = 60;\nexport const MINUTE_MS = 60 * 1000;\n\n/** Default options for Pubsub handlers */\nexport const defaultHandlerOptions = {\n retry: true,\n retryMaxAgeMinutes: undefined, // No limit on retry age by default\n memory: \"512MiB\",\n cpu: 1,\n timeoutSeconds: 20,\n maxInstances: 250,\n markEvent: false,\n vpcConnector: undefined,\n} as const;\n","import type { CloudEvent } from \"firebase-functions/v2\";\n\nexport function shouldDropEvent(\n event: CloudEvent<unknown>,\n maxAgeMinutes?: number,\n) {\n // If maxAgeMinutes is undefined, we don't drop events based on age\n if (maxAgeMinutes === undefined) {\n return false;\n }\n\n const maxAgeMs = maxAgeMinutes * 60 * 1000;\n const eventAge = Date.now() - Date.parse(event.time);\n\n if (eventAge > maxAgeMs) {\n /** Dropping event due to max age reached */\n console.error(\n new Error(\n `Dropping event ${event.type} for ${event.source} because max age (${String(maxAgeMinutes)} minutes) was reached.`,\n ),\n );\n return true;\n }\n\n return false;\n}\n\nexport function getEventAgeSeconds(event: CloudEvent<unknown>) {\n return (Date.now() - Date.parse(event.time)) / 1000;\n}\n","import { onMessagePublished as firebaseOnMessagePublished } from \"firebase-functions/v2/pubsub\";\nimport { got } from \"get-or-throw\";\nimport { defaultHandlerOptions } from \"./constants\";\nimport type {\n EventMarkingFunctions,\n HandlerOptions,\n PubsubTopicPayload,\n SchemaRecord,\n} from \"./types\";\nimport { shouldDropEvent } from \"./utils\";\n\n/**\n * Creates a factory function for generating type-safe Pubsub handlers\n *\n * @param schemas - Zod schemas for each topic\n * @param region - GCP region\n * @param eventMarkingFunctions - Optional functions for tracking processed\n * events\n * @param defaultOptions - Default options for all handlers\n * @returns A factory function for creating handlers\n */\nexport function createHandlerFactory<Schemas extends SchemaRecord<string>>(\n schemas: Schemas,\n region: string,\n eventMarkingFunctions?: EventMarkingFunctions,\n defaultOptions: HandlerOptions = defaultHandlerOptions,\n onMessagePublished = firebaseOnMessagePublished,\n) {\n return <T extends keyof Schemas & string>({\n topic,\n handler,\n options = {},\n }: {\n topic: T;\n handler: (payload: PubsubTopicPayload<Schemas, T>) => Promise<void>;\n options?: HandlerOptions;\n }) => {\n const { retryMaxAgeMinutes, markEvent, ...mergedOptions } = {\n ...defaultOptions,\n ...options,\n };\n\n // Determine if we can mark events\n const canMarkEvents = markEvent && eventMarkingFunctions !== undefined;\n\n return onMessagePublished(\n {\n ...mergedOptions,\n topic,\n region,\n },\n async (event) => {\n // Use internal shouldDropEvent implementation\n if (shouldDropEvent(event, retryMaxAgeMinutes)) {\n return;\n }\n\n // Check if event was already processed, but only if both conditions are met:\n // 1. markEvent option is true\n // 2. Event marking functions are available\n if (canMarkEvents && event.id) {\n if (await eventMarkingFunctions.isEventProcessed(event.id)) {\n /**\n * This should happen very rarely, so we log an error just to notice\n * how often it happens.\n */\n console.error(\n new Error(\n `(Not an error) Pubsub event ${event.id} was already processed`,\n ),\n );\n return;\n }\n }\n\n const schema = got(schemas, topic);\n // Type assertion to avoid unsafe assignment\n const messageData = event.data.message.json as unknown;\n const result = schema.safeParse(messageData);\n\n if (!result.success) {\n console.error(\n new Error(`Zod validation error for topic ${topic}`),\n result.error.flatten(),\n );\n return;\n }\n\n await handler(result.data as PubsubTopicPayload<Schemas, T>);\n\n // Mark event as processed, but only if both conditions are met\n if (canMarkEvents && event.id) {\n await eventMarkingFunctions.markEventAsProcessed(event.id);\n }\n },\n );\n };\n}\n","import { PubSub } from \"@google-cloud/pubsub\";\nimport type { PubsubTopicPayload, SchemaRecord } from \"./types\";\n\n/**\n * Creates a factory function that produces type-safe publishers for specific\n * topics\n *\n * @param pubsubClient - Google Cloud Pubsub client\n * @returns A factory function for creating publishers\n */\nexport function createPublisherFactory<Schemas extends SchemaRecord<string>>(\n pubsubClient: PubSub,\n) {\n return <T extends keyof Schemas & string>(topicName: T) => {\n /**\n * Publishes a message to the specified topic\n *\n * @param data - The data to publish, must conform to the topic's schema\n * @returns Promise that resolves when the message is published\n */\n return async (data: PubsubTopicPayload<Schemas, T>) => {\n const topic = pubsubClient.topic(topicName);\n await topic.publishMessage({ json: data });\n };\n };\n}\n","import type { PubSub } from \"@google-cloud/pubsub\";\nimport { onMessagePublished as firebaseOnMessagePublished } from \"firebase-functions/v2/pubsub\";\nimport { defaultHandlerOptions } from \"./constants\";\nimport { createHandlerFactory } from \"./handler\";\nimport { createPublisherFactory } from \"./publisher\";\nimport type {\n HandlerOptions,\n SchemaRecord,\n TypedPubsubClient,\n TypedPubsubOptions,\n} from \"./types\";\n\n/**\n * Creates a type-safe Pubsub client for handling messages with schema\n * validation\n *\n * @param pubsubClient - Google Cloud Pubsub client instance\n * @param schemas - Zod schemas for validating messages for each topic\n * @param region - GCP region for the Pubsub functions\n * @param options - Global defaults for all handlers\n * @returns Type-safe Pubsub client with publisher and handler factories\n */\nexport function createTypedPubsub<Schemas extends SchemaRecord<string>>({\n client,\n schemas,\n region,\n options = {},\n onMessagePublished = firebaseOnMessagePublished,\n}: {\n client: PubSub;\n schemas: Schemas;\n region: string;\n options?: TypedPubsubOptions;\n onMessagePublished?: typeof firebaseOnMessagePublished;\n}): TypedPubsubClient<Schemas> {\n // Merge default handler options\n const handlerOptions: HandlerOptions = {\n ...defaultHandlerOptions,\n ...options.defaultHandlerOptions,\n };\n\n return {\n createPublisher: createPublisherFactory<Schemas>(client),\n createHandler: createHandlerFactory(\n schemas,\n region,\n options.eventMarkingFunctions,\n handlerOptions,\n onMessagePublished,\n ),\n };\n}\n"],"mappings":";;;;;AACA,MAAa,YAAY,KAAK;;AAG9B,MAAa,wBAAwB;CACnC,OAAO;CACP,oBAAoB;CACpB,QAAQ;CACR,KAAK;CACL,gBAAgB;CAChB,cAAc;CACd,WAAW;CACX,cAAc;CACf;;;;ACXD,SAAgB,gBACd,OACA,eACA;AAEA,KAAI,kBAAkB,OACpB,QAAO;CAGT,MAAM,WAAW,gBAAgB,KAAK;AAGtC,KAFiB,KAAK,KAAK,GAAG,KAAK,MAAM,MAAM,KAAK,GAErC,UAAU;;AAEvB,UAAQ,sBACN,IAAI,MACF,kBAAkB,MAAM,KAAK,OAAO,MAAM,OAAO,oBAAoB,OAAO,cAAc,CAAC,wBAC5F,CACF;AACD,SAAO;;AAGT,QAAO;;;;;;;;;;;;;;;ACHT,SAAgB,qBACd,SACA,QACA,uBACA,iBAAiC,uBACjC,uBAAqBA,oBACrB;AACA,SAA0C,EACxC,OACA,SACA,UAAU,EAAE,OAKR;EACJ,MAAM,EAAE,oBAAoB,WAAW,GAAG,kBAAkB;GAC1D,GAAG;GACH,GAAG;GACJ;EAGD,MAAM,gBAAgB,aAAa,0BAA0B;AAE7D,SAAOC,qBACL;GACE,GAAG;GACH;GACA;GACD,EACD,OAAO,UAAU;AAEf,OAAI,gBAAgB,OAAO,mBAAmB,CAC5C;AAMF,OAAI,iBAAiB,MAAM,IACzB;QAAI,MAAM,sBAAsB,iBAAiB,MAAM,GAAG,EAAE;;;;;AAK1D,aAAQ,sBACN,IAAI,MACF,+BAA+B,MAAM,GAAG,wBACzC,CACF;AACD;;;GAIJ,MAAM,SAAS,IAAI,SAAS,MAAM;GAElC,MAAM,cAAc,MAAM,KAAK,QAAQ;GACvC,MAAM,SAAS,OAAO,UAAU,YAAY;AAE5C,OAAI,CAAC,OAAO,SAAS;AACnB,YAAQ,sBACN,IAAI,MAAM,kCAAkC,QAAQ,EACpD,OAAO,MAAM,SAAS,CACvB;AACD;;AAGF,SAAM,QAAQ,OAAO,KAAuC;AAG5D,OAAI,iBAAiB,MAAM,GACzB,OAAM,sBAAsB,qBAAqB,MAAM,GAAG;IAG/D;;;;;;;;;;;;;ACrFL,SAAgB,uBACd,cACA;AACA,SAA0C,cAAiB;;;;;;;AAOzD,SAAO,OAAO,SAAyC;AAErD,SADc,aAAa,MAAM,UAAU,CAC/B,eAAe,EAAE,MAAM,MAAM,CAAC;;;;;;;;;;;;;;;;;ACAhD,SAAgB,kBAAwD,EACtE,QACA,SACA,QACA,UAAU,EAAE,EACZ,2CAAqBC,sBAOQ;CAE7B,MAAMC,iBAAiC;EACrC,GAAG;EACH,GAAG,QAAQ;EACZ;AAED,QAAO;EACL,iBAAiB,uBAAgC,OAAO;EACxD,eAAe,qBACb,SACA,QACA,QAAQ,uBACR,gBACAC,qBACD;EACF"}