Skip to content

Commit fafe04b

Browse files
authored
Feat/idempotency logic on dreamsync adapter (#377)
* fix: idempotency in dreamsync adapter * chore: add file
1 parent 62a1329 commit fafe04b

File tree

1 file changed

+123
-0
lines changed

1 file changed

+123
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Request-scoped context system to track database operation origins
3+
* This ensures we only trigger webhooks for operations from specific services
4+
*/
5+
6+
interface OperationContext {
7+
serviceName: string;
8+
operationId: string;
9+
timestamp: number;
10+
}
11+
12+
class OperationContextManager {
13+
private static instance: OperationContextManager;
14+
private contexts: Map<string, OperationContext> = new Map();
15+
private readonly CONTEXT_EXPIRY_MS = 30_000; // 30 seconds
16+
17+
static getInstance(): OperationContextManager {
18+
if (!OperationContextManager.instance) {
19+
OperationContextManager.instance = new OperationContextManager();
20+
}
21+
return OperationContextManager.instance;
22+
}
23+
24+
/**
25+
* Create a new operation context for a specific service
26+
*/
27+
createContext(serviceName: string, operationId: string): string {
28+
const contextKey = `${serviceName}:${operationId}:${Date.now()}`;
29+
const context: OperationContext = {
30+
serviceName,
31+
operationId,
32+
timestamp: Date.now()
33+
};
34+
35+
this.contexts.set(contextKey, context);
36+
37+
// Clean up expired contexts
38+
this.cleanupExpiredContexts();
39+
40+
console.log(`🔧 Created operation context: ${contextKey}`);
41+
return contextKey;
42+
}
43+
44+
/**
45+
* Check if an operation should be processed based on context
46+
* Only applies protection to groups and messages entities
47+
*/
48+
shouldProcessOperation(entityId: string, tableName: string): boolean {
49+
// Only apply context protection to groups and messages
50+
const protectedEntities = ['groups', 'messages'];
51+
if (!protectedEntities.includes(tableName.toLowerCase())) {
52+
console.log(`✅ Entity ${tableName} is not protected, allowing operation for ${entityId}`);
53+
return true;
54+
}
55+
56+
// Look for any active ConsentService context
57+
for (const [contextKey, context] of this.contexts.entries()) {
58+
if (context.serviceName === 'ConsentService') {
59+
console.log(`✅ Found ConsentService context: ${contextKey}, processing protected operation for ${tableName}:${entityId}`);
60+
return true;
61+
}
62+
}
63+
64+
console.log(`❌ No ConsentService context found, skipping protected operation for ${tableName}:${entityId}`);
65+
return false;
66+
}
67+
68+
/**
69+
* Remove a specific context
70+
*/
71+
removeContext(contextKey: string): void {
72+
if (this.contexts.delete(contextKey)) {
73+
console.log(`🗑️ Removed operation context: ${contextKey}`);
74+
}
75+
}
76+
77+
/**
78+
* Clean up expired contexts
79+
*/
80+
private cleanupExpiredContexts(): void {
81+
const now = Date.now();
82+
for (const [contextKey, context] of this.contexts.entries()) {
83+
if (now - context.timestamp > this.CONTEXT_EXPIRY_MS) {
84+
this.contexts.delete(contextKey);
85+
console.log(`🧹 Cleaned up expired context: ${contextKey}`);
86+
}
87+
}
88+
}
89+
90+
/**
91+
* Get all active contexts (for debugging)
92+
*/
93+
getActiveContexts(): Map<string, OperationContext> {
94+
return new Map(this.contexts);
95+
}
96+
}
97+
98+
export const operationContextManager = OperationContextManager.getInstance();
99+
100+
/**
101+
* Decorator to wrap database operations with context
102+
*/
103+
export function withOperationContext<T>(
104+
serviceName: string,
105+
operationId: string,
106+
operation: () => Promise<T>
107+
): Promise<T> {
108+
const contextKey = operationContextManager.createContext(serviceName, operationId);
109+
110+
return operation().finally(() => {
111+
// Remove context after operation completes
112+
setTimeout(() => {
113+
operationContextManager.removeContext(contextKey);
114+
}, 1000); // Small delay to ensure all database operations complete
115+
});
116+
}
117+
118+
/**
119+
* Check if current operation should be processed by webhooks
120+
*/
121+
export function shouldProcessWebhook(entityId: string, tableName: string): boolean {
122+
return operationContextManager.shouldProcessOperation(entityId, tableName);
123+
}

0 commit comments

Comments
 (0)