@codecompose/typed-pubsub
Version:
A type-safe pub/sub abstraction for Google Cloud and Firebase
128 lines (122 loc) • 4.2 kB
JavaScript
import { onMessagePublished } from "firebase-functions/v2/pubsub";
import { got } from "get-or-throw";
import { PubSub } from "@google-cloud/pubsub";
//#region src/constants.ts
const MINUTE_MS = 60 * 1e3;
/** Default options for Pubsub handlers */
const defaultHandlerOptions = {
retry: true,
retryMaxAgeMinutes: void 0,
memory: "512MiB",
cpu: 1,
timeoutSeconds: 20,
maxInstances: 250,
markEvent: false,
vpcConnector: void 0
};
//#endregion
//#region src/utils/should-drop-event.ts
function shouldDropEvent(event, maxAgeMinutes) {
if (maxAgeMinutes === void 0) return false;
const maxAgeMs = maxAgeMinutes * 60 * 1e3;
if (Date.now() - Date.parse(event.time) > maxAgeMs) {
/** Dropping event due to max age reached */
console.error(/* @__PURE__ */ new Error(`Dropping event ${event.type} for ${event.source} because max age (${String(maxAgeMinutes)} minutes) was reached.`));
return true;
}
return false;
}
//#endregion
//#region src/handler.ts
/**
* Creates a factory function for generating type-safe Pubsub handlers
*
* @param schemas - Zod schemas for each topic
* @param region - GCP region
* @param eventMarkingFunctions - Optional functions for tracking processed
* events
* @param defaultOptions - Default options for all handlers
* @returns A factory function for creating handlers
*/
function createHandlerFactory(schemas, region, eventMarkingFunctions, defaultOptions = defaultHandlerOptions, onMessagePublished$1 = onMessagePublished) {
return ({ topic, handler, options = {} }) => {
const { retryMaxAgeMinutes, markEvent, ...mergedOptions } = {
...defaultOptions,
...options
};
const canMarkEvents = markEvent && eventMarkingFunctions !== void 0;
return onMessagePublished$1({
...mergedOptions,
topic,
region
}, async (event) => {
if (shouldDropEvent(event, retryMaxAgeMinutes)) return;
if (canMarkEvents && event.id) {
if (await eventMarkingFunctions.isEventProcessed(event.id)) {
/**
* This should happen very rarely, so we log an error just to notice
* how often it happens.
*/
console.error(/* @__PURE__ */ new Error(`(Not an error) Pubsub event ${event.id} was already processed`));
return;
}
}
const schema = got(schemas, topic);
const messageData = event.data.message.json;
const result = schema.safeParse(messageData);
if (!result.success) {
console.error(/* @__PURE__ */ new Error(`Zod validation error for topic ${topic}`), result.error.flatten());
return;
}
await handler(result.data);
if (canMarkEvents && event.id) await eventMarkingFunctions.markEventAsProcessed(event.id);
});
};
}
//#endregion
//#region src/publisher.ts
/**
* Creates a factory function that produces type-safe publishers for specific
* topics
*
* @param pubsubClient - Google Cloud Pubsub client
* @returns A factory function for creating publishers
*/
function createPublisherFactory(pubsubClient) {
return (topicName) => {
/**
* Publishes a message to the specified topic
*
* @param data - The data to publish, must conform to the topic's schema
* @returns Promise that resolves when the message is published
*/
return async (data) => {
await pubsubClient.topic(topicName).publishMessage({ json: data });
};
};
}
//#endregion
//#region src/factory.ts
/**
* Creates a type-safe Pubsub client for handling messages with schema
* validation
*
* @param pubsubClient - Google Cloud Pubsub client instance
* @param schemas - Zod schemas for validating messages for each topic
* @param region - GCP region for the Pubsub functions
* @param options - Global defaults for all handlers
* @returns Type-safe Pubsub client with publisher and handler factories
*/
function createTypedPubsub({ client, schemas, region, options = {}, onMessagePublished: onMessagePublished$1 = onMessagePublished }) {
const handlerOptions = {
...defaultHandlerOptions,
...options.defaultHandlerOptions
};
return {
createPublisher: createPublisherFactory(client),
createHandler: createHandlerFactory(schemas, region, options.eventMarkingFunctions, handlerOptions, onMessagePublished$1)
};
}
//#endregion
export { createTypedPubsub };
//# sourceMappingURL=index.mjs.map