fix: remove old event queue, cleaned up session handling, remove hacks

This commit is contained in:
Carl-Gerhard Lindesvärd
2025-10-09 09:25:52 +02:00
parent a11f87dc3c
commit e7c21bc92c
16 changed files with 202 additions and 245 deletions

View File

@@ -25,7 +25,7 @@ export class SessionBuffer extends BaseBuffer {
this.redis = getRedisCache();
}
async getExistingSession(sessionId: string) {
public async getExistingSession(sessionId: string) {
const hit = await this.redis.get(`session:${sessionId}`);
if (hit) {

View File

@@ -69,7 +69,8 @@ export type INotificationRuleCached = Awaited<
ReturnType<typeof getNotificationRulesByProjectId>
>[number];
export const getNotificationRulesByProjectId = cacheable(
function getNotificationRulesByProjectId(projectId: string) {
'getNotificationRulesByProjectId',
(projectId: string) => {
return db.notificationRule.findMany({
where: {
projectId,
@@ -330,6 +331,17 @@ export async function checkNotificationRulesForEvent(
);
}
const isFunnelRule = (rule: INotificationRuleCached) =>
rule.config.type === 'funnel';
export function getHasFunnelRules(rules: INotificationRuleCached[]) {
return rules.some(isFunnelRule);
}
export function getFunnelRules(rules: INotificationRuleCached[]) {
return rules.filter(isFunnelRule);
}
export async function checkNotificationRulesForSessionEnd(
events: IServiceEvent[],
) {
@@ -344,8 +356,7 @@ export async function checkNotificationRulesForSessionEnd(
getNotificationRulesByProjectId(projectId),
]);
const funnelRules = rules.filter((rule) => rule.config.type === 'funnel');
const funnelRules = getFunnelRules(rules);
const notificationPromises = funnelRules.flatMap((rule) => {
// Match funnel events
let funnelIndex = 0;

View File

@@ -10,7 +10,7 @@
"@openpanel/logger": "workspace:*",
"@openpanel/redis": "workspace:*",
"bullmq": "^5.8.7",
"groupmq": "1.0.0-next.17"
"groupmq": "1.0.0-next.18"
},
"devDependencies": {
"@openpanel/sdk": "workspace:*",

View File

@@ -1,6 +1,10 @@
import { Queue, QueueEvents } from 'bullmq';
import type { IServiceEvent, Prisma } from '@openpanel/db';
import type {
IServiceCreateEventPayload,
IServiceEvent,
Prisma,
} from '@openpanel/db';
import { createLogger } from '@openpanel/logger';
import { getRedisGroupQueue, getRedisQueue } from '@openpanel/redis';
import type { TrackPayload } from '@openpanel/sdk';
@@ -32,16 +36,10 @@ export interface EventsQueuePayloadCreateEvent {
type: 'createEvent';
payload: Omit<IServiceEvent, 'id'>;
}
type SessionEndRequired =
| 'sessionId'
| 'deviceId'
| 'profileId'
| 'projectId'
| 'createdAt';
export interface EventsQueuePayloadCreateSessionEnd {
type: 'createSessionEnd';
payload: Partial<Omit<IServiceEvent, SessionEndRequired>> &
Pick<IServiceEvent, SessionEndRequired>;
payload: IServiceCreateEventPayload;
}
// TODO: Rename `EventsQueuePayloadCreateSessionEnd`
@@ -95,18 +93,6 @@ export type MiscQueuePayload = MiscQueuePayloadTrialEndingSoon;
export type CronQueueType = CronQueuePayload['type'];
export const eventsQueue = new Queue<EventsQueuePayload>('events', {
connection: getRedisQueue(),
defaultJobOptions: {
removeOnComplete: 10,
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000,
},
},
});
const orderingWindowMs = Number.parseInt(
process.env.ORDERING_WINDOW_MS || '50',
10,

View File

@@ -23,33 +23,57 @@ export async function getCache<T>(
return data;
}
export function cacheable<T extends (...args: any) => any>(
fn: T,
expireInSec: number,
) {
const cachePrefix = `cachable:${fn.name}`;
function stringify(obj: unknown): string {
if (obj === null) return 'null';
if (obj === undefined) return 'undefined';
if (typeof obj === 'boolean') return obj ? 'true' : 'false';
if (typeof obj === 'number') return String(obj);
if (typeof obj === 'string') return obj;
if (typeof obj === 'function') return obj.toString();
function stringify(obj: unknown): string {
if (obj === null) return 'null';
if (obj === undefined) return 'undefined';
if (typeof obj === 'boolean') return obj ? 'true' : 'false';
if (typeof obj === 'number') return String(obj);
if (typeof obj === 'string') return obj;
if (typeof obj === 'function') return obj.toString();
if (Array.isArray(obj)) {
return `[${obj.map(stringify).join(',')}]`;
}
if (typeof obj === 'object') {
const pairs = Object.entries(obj)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}:${stringify(value)}`);
return pairs.join(':');
}
// Fallback for any other types
return String(obj);
if (Array.isArray(obj)) {
return `[${obj.map(stringify).join(',')}]`;
}
if (typeof obj === 'object') {
const pairs = Object.entries(obj)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}:${stringify(value)}`);
return pairs.join(':');
}
// Fallback for any other types
return String(obj);
}
export function cacheable<T extends (...args: any) => any>(
fnOrName: T | string,
fnOrExpireInSec: number | T,
_expireInSec?: number,
) {
const name = typeof fnOrName === 'string' ? fnOrName : fnOrName.name;
const fn =
typeof fnOrName === 'function'
? fnOrName
: typeof fnOrExpireInSec === 'function'
? fnOrExpireInSec
: null;
const expireInSec =
typeof fnOrExpireInSec === 'number'
? fnOrExpireInSec
: typeof _expireInSec === 'number'
? _expireInSec
: null;
if (typeof fn !== 'function') {
throw new Error('fn is not a function');
}
if (typeof expireInSec !== 'number') {
throw new Error('expireInSec is not a number');
}
const cachePrefix = `cachable:${name}`;
const getKey = (...args: Parameters<T>) =>
`${cachePrefix}:${stringify(args)}`;
const cachedFn = async (