Skip to content

Commit 08c539a

Browse files
authored
Merge branch 'main' into Implement-subscription-credit-system-and-account-balance
2 parents f51f59c + 78d0e80 commit 08c539a

30 files changed

Lines changed: 3374 additions & 1256 deletions

babel.config.test.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,5 @@ module.exports = function (api) {
33
return {
44
presets: [['babel-preset-expo', { unstable_transformProfile: 'default' }]],
55
plugins: ['@babel/plugin-transform-flow-strip-types'],
6-
overrides: [
7-
{
8-
plugins: ['babel-plugin-syntax-hermes-parser'],
9-
test: (filename) => {
10-
return (
11-
!filename ||
12-
(!filename.includes('node_modules/react-native/Libraries/NativeComponent') &&
13-
!filename.endsWith('.ts') &&
14-
!filename.endsWith('.tsx'))
15-
);
16-
},
17-
},
18-
],
196
};
207
};

backend/services/notification/__tests__/dunningEmailSequences.test.ts

Lines changed: 516 additions & 0 deletions
Large diffs are not rendered by default.

backend/services/notification/alerting.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,8 @@
33
* Channels are pluggable; add as many as needed.
44
*/
55

6-
<<<<<<< HEAD:backend/services/alerting.ts
7-
import { logger } from './logging';
8-
import type { Alert, AlertChannelConfig } from './types';
9-
=======
6+
import { logger } from '../../services/logging';
107
import type { Alert, AlertChannelConfig } from '../shared/types';
11-
>>>>>>> main:backend/services/notification/alerting.ts
128

139
export interface AlertDispatcher {
1410
send(alert: Alert): Promise<void>;
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* CDN Edge Caching Service — SubTrackr
3+
*
4+
* Manages edge caching for API responses with TTL, purge, and invalidation.
5+
*/
6+
7+
export interface CdnConfig {
8+
defaultTtlSeconds: number;
9+
maxTtlSeconds: number;
10+
staleWhileRevalidateSeconds: number;
11+
purgeBatchSize: number;
12+
}
13+
14+
export interface CacheEntry<T = unknown> {
15+
key: string;
16+
data: T;
17+
headers: Record<string, string>;
18+
cachedAt: number;
19+
ttlMs: number;
20+
tags: string[];
21+
}
22+
23+
export interface PurgeRequest {
24+
patterns: string[];
25+
tags?: string[];
26+
}
27+
28+
export interface PurgeResult {
29+
purgedCount: number;
30+
patterns: string[];
31+
}
32+
33+
export interface CdnMetrics {
34+
hits: number;
35+
misses: number;
36+
purges: number;
37+
hitRate: number;
38+
}
39+
40+
export class CdnService {
41+
private cache = new Map<string, CacheEntry>();
42+
private tagIndex = new Map<string, Set<string>>();
43+
private config: CdnConfig;
44+
private metrics: CdnMetrics = { hits: 0, misses: 0, purges: 0, hitRate: 0 };
45+
46+
constructor(config: Partial<CdnConfig> = {}) {
47+
this.config = {
48+
defaultTtlSeconds: config.defaultTtlSeconds ?? 300,
49+
maxTtlSeconds: config.maxTtlSeconds ?? 86400,
50+
staleWhileRevalidateSeconds: config.staleWhileRevalidateSeconds ?? 60,
51+
purgeBatchSize: config.purgeBatchSize ?? 100,
52+
};
53+
}
54+
55+
get<T>(key: string): CacheEntry<T> | null {
56+
const entry = this.cache.get(key) as CacheEntry<T> | undefined;
57+
if (!entry) {
58+
this.metrics.misses++;
59+
this.updateHitRate();
60+
return null;
61+
}
62+
63+
const now = Date.now();
64+
if (now > entry.cachedAt + entry.ttlMs) {
65+
this.cache.delete(key);
66+
this.removeFromTagIndex(key, entry.tags);
67+
this.metrics.misses++;
68+
this.updateHitRate();
69+
return null;
70+
}
71+
72+
this.metrics.hits++;
73+
this.updateHitRate();
74+
return entry;
75+
}
76+
77+
set<T>(key: string, data: T, options: { ttlSeconds?: number; tags?: string[] } = {}): void {
78+
const ttlMs = Math.min(
79+
(options.ttlSeconds ?? this.config.defaultTtlSeconds) * 1000,
80+
this.config.maxTtlSeconds * 1000,
81+
);
82+
83+
const entry: CacheEntry<T> = {
84+
key,
85+
data,
86+
headers: {
87+
'cache-control': `public, max-age=${Math.floor(ttlMs / 1000)}, stale-while-revalidate=${this.config.staleWhileRevalidateSeconds}`,
88+
'cdn-cache-status': 'HIT',
89+
},
90+
cachedAt: Date.now(),
91+
ttlMs,
92+
tags: options.tags ?? [],
93+
};
94+
95+
this.cache.set(key, entry);
96+
for (const tag of entry.tags) {
97+
if (!this.tagIndex.has(tag)) this.tagIndex.set(tag, new Set());
98+
this.tagIndex.get(tag)!.add(key);
99+
}
100+
}
101+
102+
purge(request: PurgeRequest): PurgeResult {
103+
let purgedCount = 0;
104+
const purgedPatterns: string[] = [];
105+
106+
for (const pattern of request.patterns) {
107+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
108+
for (const [key, entry] of this.cache) {
109+
if (regex.test(key)) {
110+
this.cache.delete(key);
111+
this.removeFromTagIndex(key, entry.tags);
112+
purgedCount++;
113+
}
114+
}
115+
purgedPatterns.push(pattern);
116+
}
117+
118+
if (request.tags) {
119+
for (const tag of request.tags) {
120+
const keys = this.tagIndex.get(tag);
121+
if (keys) {
122+
for (const key of keys) {
123+
this.cache.delete(key);
124+
purgedCount++;
125+
}
126+
this.tagIndex.delete(tag);
127+
}
128+
}
129+
}
130+
131+
this.metrics.purges++;
132+
return { purgedCount, patterns: purgedPatterns };
133+
}
134+
135+
invalidate(key: string): boolean {
136+
const entry = this.cache.get(key);
137+
if (entry) {
138+
this.removeFromTagIndex(key, entry.tags);
139+
this.cache.delete(key);
140+
return true;
141+
}
142+
return false;
143+
}
144+
145+
getMetrics(): CdnMetrics {
146+
return { ...this.metrics };
147+
}
148+
149+
private removeFromTagIndex(key: string, tags: string[]): void {
150+
for (const tag of tags) {
151+
this.tagIndex.get(tag)?.delete(key);
152+
}
153+
}
154+
155+
private updateHitRate(): void {
156+
const total = this.metrics.hits + this.metrics.misses;
157+
this.metrics.hitRate = total > 0 ? this.metrics.hits / total : 0;
158+
}
159+
}
160+
161+
export const cdnService = new CdnService();

backend/services/shared/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,19 @@ export type {
169169
LeakRecord,
170170
PoolTuningRecommendation,
171171
} from './poolMonitor';
172+
173+
// ── Background Job Queue (#990) ──────────────────────────────────────────────
174+
export { PriorityQueue, jobQueue } from './jobQueue';
175+
export type { Job, JobHandler, QueueConfig, QueueMetrics, JobStatus, JobPriority } from './jobQueue';
176+
177+
// ── CDN Edge Caching (#991) ──────────────────────────────────────────────────
178+
export { CdnService, cdnService } from './cdnService';
179+
export type { CdnConfig, CacheEntry, PurgeRequest, PurgeResult, CdnMetrics } from './cdnService';
180+
181+
// ── WebSocket Connection Pool (#994) ──────────────────────────────────────────
182+
export { WsConnectionPool } from './wsConnectionPool';
183+
export type { WsPoolConfig, WsConnection, WsMessage, WsPoolMetrics } from './wsConnectionPool';
184+
185+
// ── Read Replica Router (#997) ────────────────────────────────────────────────
186+
export { ReadReplicaRouter } from './readReplicaRouter';
187+
export type { ReplicaConfig, ReplicaHealth, ReadRouteOptions, QueryRoute } from './readReplicaRouter';

0 commit comments

Comments
 (0)