367 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Injectable, OnModuleInit, OnModuleDestroy, Inject } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import PubSubApiClientPkg from "salesforce-pubsub-api-client";
import { SalesforceConnection } from "../services/salesforce-connection.service";
import { ProvisioningQueueService } from "@bff/modules/orders/queue/provisioning.queue";
import { CacheService } from "@bff/infra/cache/cache.service";
import {
replayKey as sfReplayKey,
statusKey as sfStatusKey,
latestSeenKey as sfLatestSeenKey,
} from "./event-keys.util";
import type {
SalesforcePubSubEvent,
SalesforcePubSubError,
SalesforcePubSubSubscription,
SalesforcePubSubCallbackType,
SalesforcePubSubUnknownData,
} from "../types/pubsub-events.types";
type SubscribeCallback = (
subscription: SalesforcePubSubSubscription,
callbackType: SalesforcePubSubCallbackType,
data: SalesforcePubSubEvent | SalesforcePubSubError | SalesforcePubSubUnknownData
) => void | Promise<void>;
interface PubSubClient {
connect(): Promise<void>;
subscribe(topic: string, cb: SubscribeCallback, numRequested?: number): Promise<void>;
subscribeFromReplayId(
topic: string,
cb: SubscribeCallback,
numRequested: number | null,
replayId: number
): Promise<void>;
subscribeFromEarliestEvent(
topic: string,
cb: SubscribeCallback,
numRequested?: number
): Promise<void>;
requestAdditionalEvents(topic: string, numRequested: number): Promise<void>;
close(): Promise<void>;
}
type PubSubCtor = new (opts: {
authType: string;
accessToken: string;
instanceUrl: string;
pubSubEndpoint: string;
}) => PubSubClient;
@Injectable()
export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy {
private client: PubSubClient | null = null;
private clientAccessToken: string | null = null;
private channel!: string;
private replayCorruptionRecovered = false;
private subscribeCallback!: SubscribeCallback;
private pubSubCtor: PubSubCtor | null = null;
private clientBuildInFlight: Promise<PubSubClient> | null = null;
constructor(
private readonly config: ConfigService,
private readonly sfConn: SalesforceConnection,
private readonly provisioningQueue: ProvisioningQueueService,
private readonly cache: CacheService,
@Inject(Logger) private readonly logger: Logger
) {}
async onModuleInit(): Promise<void> {
const enabled = this.config.get("SF_EVENTS_ENABLED", "false") === "true";
if (!enabled) {
this.logger.log("Salesforce Pub/Sub subscriber disabled", { enabled });
return;
}
this.channel = this.config.get<string>(
"SF_PROVISION_EVENT_CHANNEL",
"/event/Order_Fulfilment_Requested__e"
);
try {
this.subscribeCallback = this.buildSubscribeCallback();
await this.subscribeWithPolicy(true);
} catch (error) {
this.logger.error("Salesforce Pub/Sub subscription failed", {
error: error instanceof Error ? error.message : String(error),
});
try {
await this.cache.set(sfStatusKey(this.channel || "/event/OrderProvisionRequested__e"), {
status: "disconnected",
since: Date.now(),
});
} catch (cacheErr) {
this.logger.warn("Failed to set SF Pub/Sub disconnected status", {
error: cacheErr instanceof Error ? cacheErr.message : String(cacheErr),
});
}
}
}
async onModuleDestroy(): Promise<void> {
try {
await this.safeCloseClient();
await this.cache.set(sfStatusKey(this.channel), {
status: "disconnected",
since: Date.now(),
});
} catch (error) {
this.logger.warn("Error closing Salesforce Pub/Sub client", {
error: error instanceof Error ? error.message : String(error),
});
}
}
private buildSubscribeCallback(): SubscribeCallback {
return async (subscription, callbackType, data) => {
try {
const argTypes = [typeof subscription, typeof callbackType, typeof data];
const type = callbackType;
const typeNorm = String(type || "").toLowerCase();
const topic = subscription.topicName || this.channel;
if (typeNorm === "data" || typeNorm === "event") {
const event = data as SalesforcePubSubEvent;
this.logger.debug("SF Pub/Sub data callback received", {
topic,
argTypes,
hasPayload: Boolean(event?.payload),
});
const payload = event?.payload;
const orderIdVal = payload?.["OrderId__c"] ?? payload?.["OrderId"];
const orderId = typeof orderIdVal === "string" ? orderIdVal : undefined;
if (!orderId) {
this.logger.warn("Pub/Sub event missing OrderId__c; skipping", {
argTypes,
topic,
payloadKeys: payload ? Object.keys(payload) : [],
});
const depth = await this.provisioningQueue.depth();
if (depth < this.getMaxQueueSize()) {
const activeClient = this.client;
if (activeClient) {
await activeClient.requestAdditionalEvents(topic, 1);
}
}
return;
}
const replayVal = (event as { replayId?: unknown })?.replayId;
const idempotencyKey =
typeof replayVal === "number" || typeof replayVal === "string"
? String(replayVal)
: String(Date.now());
const pubsubReplayId = typeof replayVal === "number" ? replayVal : undefined;
await this.provisioningQueue.enqueue({
sfOrderId: orderId,
idempotencyKey,
pubsubReplayId,
});
this.logger.log("Enqueued provisioning job from SF event", {
sfOrderId: orderId,
replayId: pubsubReplayId,
topic,
});
} else if (typeNorm === "lastevent") {
const depth = await this.provisioningQueue.depth();
const available = Math.max(0, this.getMaxQueueSize() - depth);
const desired = Math.max(0, Math.min(this.getNumRequested(), available));
if (desired > 0) {
const activeClient = this.client;
if (activeClient) {
await activeClient.requestAdditionalEvents(topic, desired);
}
}
} else if (typeNorm === "grpckeepalive") {
const latestVal = (data as { latestReplayId?: unknown })?.latestReplayId;
const latest = typeof latestVal === "number" ? latestVal : undefined;
if (typeof latest === "number") {
await this.cache.set(sfLatestSeenKey(this.channel), {
id: String(latest),
at: Date.now(),
});
}
} else if (typeNorm === "grpcstatus" || typeNorm === "end") {
// Informational no action required
} else if (typeNorm === "error") {
this.logger.warn("SF Pub/Sub stream error", { topic, data });
try {
const errorData = data as SalesforcePubSubError;
const details = errorData.details || "";
const metadata = errorData.metadata || {};
const errorCodes = Array.isArray(metadata["error-code"]) ? metadata["error-code"] : [];
const hasCorruptionCode = errorCodes.some(code =>
String(code).includes("replayid.corrupted")
);
const mentionsReplayValidation = /Replay ID validation failed/i.test(details);
if (
(hasCorruptionCode || mentionsReplayValidation) &&
!this.replayCorruptionRecovered
) {
this.replayCorruptionRecovered = true;
const key = sfReplayKey(this.channel);
await this.cache.del(key);
this.logger.warn(
"Cleared invalid Salesforce Pub/Sub replay cursor; retrying subscription",
{
channel: this.channel,
key,
}
);
}
} catch (recoveryErr) {
this.logger.warn("SF Pub/Sub replay corruption auto-recovery failed", {
error: recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr),
});
} finally {
await this.recoverFromStreamError();
}
} else {
const maybeEvent = data as SalesforcePubSubEvent | undefined;
const hasPayload = Boolean(maybeEvent?.payload);
this.logger.debug("SF Pub/Sub callback ignored (unknown type)", {
type,
topic,
argTypes,
hasPayload,
});
}
} catch (err) {
this.logger.error("Pub/Sub subscribe callback failed", {
error: err instanceof Error ? err.message : String(err),
});
}
};
}
private getNumRequested(): number {
return Number(this.config.get("SF_PUBSUB_NUM_REQUESTED", "50")) || 50;
}
private getMaxQueueSize(): number {
return Number(this.config.get("SF_PUBSUB_QUEUE_MAX", "100")) || 100;
}
private getPubSubCtor(): PubSubCtor {
if (this.pubSubCtor) {
return this.pubSubCtor;
}
const maybeCtor =
(PubSubApiClientPkg as { default?: unknown })?.default ?? (PubSubApiClientPkg as unknown);
this.pubSubCtor = maybeCtor as PubSubCtor;
return this.pubSubCtor;
}
private async ensureClient(forceRefresh = false): Promise<PubSubClient> {
if (this.clientBuildInFlight && !forceRefresh) {
return this.clientBuildInFlight;
}
this.clientBuildInFlight = (async () => {
await this.sfConn.ensureConnected();
const accessToken = this.sfConn.getAccessToken();
const instanceUrl = this.sfConn.getInstanceUrl();
if (!accessToken || !instanceUrl) {
throw new Error("Salesforce access token || instance URL unavailable");
}
const tokenChanged = this.clientAccessToken !== accessToken;
if (!this.client || forceRefresh || tokenChanged) {
await this.safeCloseClient();
const endpoint = this.config.get<string>(
"SF_PUBSUB_ENDPOINT",
"api.pubsub.salesforce.com:7443"
);
const Ctor = this.getPubSubCtor();
const client = new Ctor({
authType: "user-supplied",
accessToken,
instanceUrl,
pubSubEndpoint: endpoint,
});
await client.connect();
this.client = client;
this.clientAccessToken = accessToken;
this.replayCorruptionRecovered = false;
}
return this.client;
})();
try {
return await this.clientBuildInFlight;
} finally {
this.clientBuildInFlight = null;
}
}
private async subscribeWithPolicy(forceClientRefresh = false): Promise<void> {
if (!this.subscribeCallback) {
throw new Error("Subscribe callback not initialized");
}
await this.cache.set(sfStatusKey(this.channel), {
status: "connecting",
since: Date.now(),
});
const client = await this.ensureClient(forceClientRefresh);
const replayMode = this.config.get<string>("SF_EVENTS_REPLAY", "LATEST");
const replayKey = sfReplayKey(this.channel);
const storedReplay = replayMode !== "ALL" ? await this.cache.get<string>(replayKey) : null;
const numRequested = this.getNumRequested();
if (storedReplay && replayMode !== "ALL") {
await client.subscribeFromReplayId(
this.channel,
this.subscribeCallback,
numRequested,
Number(storedReplay)
);
} else if (replayMode === "ALL") {
await client.subscribeFromEarliestEvent(this.channel, this.subscribeCallback, numRequested);
} else {
await client.subscribe(this.channel, this.subscribeCallback, numRequested);
}
await this.cache.set(sfStatusKey(this.channel), {
status: "connected",
since: Date.now(),
});
this.logger.log("Salesforce Pub/Sub subscription active", { channel: this.channel });
}
private async recoverFromStreamError(): Promise<void> {
await this.cache.set(sfStatusKey(this.channel), {
status: "reconnecting",
since: Date.now(),
});
await this.safeCloseClient();
await this.subscribeWithPolicy(true);
}
private async safeCloseClient(): Promise<void> {
if (!this.client) {
return;
}
try {
await this.client.close();
} catch (error) {
this.logger.warn("Failed to close Salesforce Pub/Sub client", {
error: error instanceof Error ? error.message : String(error),
});
} finally {
this.client = null;
this.clientAccessToken = null;
}
}
}