Skip to content

Commit 7d37269

Browse files
authored
🔧 update: enhance platform source detection and add unit tests
2 parents 0f8db69 + f924b27 commit 7d37269

10 files changed

Lines changed: 694 additions & 34 deletions

File tree

.contributerc.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"workflow": "clean-flow",
3+
"role": "maintainer",
4+
"mainBranch": "main",
5+
"devBranch": "dev",
6+
"upstream": "upstream",
7+
"origin": "origin",
8+
"branchPrefixes": [
9+
"feature",
10+
"fix",
11+
"docs",
12+
"chore",
13+
"test",
14+
"refactor"
15+
],
16+
"commitConvention": "clean-commit"
17+
}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,5 @@ yarn.lock
152152
context/
153153
# Snyk Security Extension - AI Rules (auto-generated)
154154
.github/instructions/snyk_rules.instructions.md
155+
156+
.contributerc.json

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"test:coverage": "vitest run --coverage"
4141
},
4242
"dependencies": {
43-
"@wgtechlabs/log-engine": "2.2.0",
43+
"@wgtechlabs/log-engine": "2.3.1",
4444
"dotenv": "^16.4.0",
4545
"express": "^4.21.0",
4646
"express-validator": "^7.3.1",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ import { validateEvent } from './middleware/validation';
1111
import { WebhookRequest } from './types';
1212
import { RedisService } from './services/redisService';
1313

14-
// Configure LogEngine to use only local time (no ISO timestamp)
14+
// Configure LogEngine with no timestamps (emoji + level + message only)
1515
LogEngine.configure({
16-
mode: LogMode.DEBUG
16+
mode: LogMode.DEBUG,
17+
format: {
18+
includeIsoTimestamp: false,
19+
includeLocalTime: false
20+
}
1721
});
1822

1923
const app = express();

src/config/redis.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const redisEventConfig = {
3636
// Event tracking configuration
3737
eventTtl: 259200, // 3 days (72 hours * 60 minutes * 60 seconds)
3838
keyPrefix: 'unthread:eventid:',
39+
fingerprintPrefix: 'unthread:fp:',
3940
};
4041

4142
// Create Redis client for v4.x - use URL string directly with timeout

src/controllers/webhookController.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class WebhookController {
1717
static initializeBackgroundProcessor(): void {
1818
if (!WebhookController.backgroundProcessor) {
1919
WebhookController.backgroundProcessor = new WebhookController();
20-
LogEngine.log('🔄 Background webhook processor initialized');
20+
LogEngine.log('Background webhook processor initialized');
2121
}
2222
}
2323

@@ -34,7 +34,7 @@ export class WebhookController {
3434
const { event, eventId } = req.body;
3535

3636
// Log raw incoming webhook data
37-
LogEngine.debug(`🌐 RAW WEBHOOK RECEIVED:`, {
37+
LogEngine.debug(`RAW WEBHOOK RECEIVED:`, {
3838
eventId,
3939
completeRawData: req.body
4040
});
@@ -48,7 +48,7 @@ export class WebhookController {
4848
// Quick validation only - no heavy processing
4949
const validationResult = this.webhookService.validateEvent(req.body);
5050
if (!validationResult.isValid) {
51-
LogEngine.error(`Event validation failed:`, validationResult.errors);
51+
LogEngine.error(`Event validation failed:`, validationResult.errors);
5252
return res.status(400).json({
5353
error: 'Invalid event structure',
5454
details: validationResult.errors
@@ -71,7 +71,7 @@ export class WebhookController {
7171
// Queue event for background processing (non-blocking)
7272
this.queueEventForBackgroundProcessing(req.body, requestId);
7373

74-
LogEngine.debug(`Immediate response sent`, {
74+
LogEngine.debug(`Immediate response sent`, {
7575
eventId,
7676
requestId,
7777
responseTime: `${responseTime}ms`
@@ -81,7 +81,7 @@ export class WebhookController {
8181

8282
} catch (error) {
8383
const responseTime = Date.now() - startTime;
84-
LogEngine.error(`💥 Error handling webhook: ${error}`);
84+
LogEngine.error(`Error handling webhook: ${error}`);
8585
return res.status(500).json({
8686
error: 'Internal server error',
8787
responseTime: `${responseTime}ms`,
@@ -106,12 +106,12 @@ export class WebhookController {
106106
await this.processEventInBackground(event, requestId);
107107
}
108108
} catch (error) {
109-
LogEngine.error(`💥 Background processing failed:`, {
109+
LogEngine.error(`Background processing failed:`, {
110110
requestId,
111111
eventId: event?.eventId,
112-
error: error instanceof Error ? error.message : 'Unknown error'
113-
});
114-
}
112+
error: error instanceof Error ? error.message : 'Unknown error'
113+
});
114+
}
115115
})();
116116
});
117117
}
@@ -123,7 +123,7 @@ export class WebhookController {
123123
const startTime = Date.now();
124124

125125
try {
126-
LogEngine.debug(`🔄 Background processing started`, {
126+
LogEngine.debug(`Background processing started`, {
127127
eventId: event?.eventId,
128128
requestId
129129
});
@@ -133,15 +133,15 @@ export class WebhookController {
133133
await this.webhookService.processEvent(event);
134134

135135
const processingTime = Date.now() - startTime;
136-
LogEngine.info(`Background processing completed`, {
136+
LogEngine.info(`Background processing completed`, {
137137
eventId: event?.eventId,
138138
requestId,
139139
processingTime: `${processingTime}ms`
140140
});
141141

142142
} catch (error) {
143143
const processingTime = Date.now() - startTime;
144-
LogEngine.error(`💥 Background processing failed:`, {
144+
LogEngine.error(`Background processing failed:`, {
145145
eventId: event?.eventId,
146146
requestId,
147147
processingTime: `${processingTime}ms`,

src/services/redisService.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,31 +46,53 @@ export class RedisService {
4646
const eventJson = JSON.stringify(event);
4747

4848
// Log the complete transformed event data
49-
LogEngine.debug(`TRANSFORMED WEBHOOK EVENT:`, {
49+
LogEngine.debug(`TRANSFORMED WEBHOOK EVENT:`, {
5050
eventId: event.data?.eventId || 'unknown',
5151
completeTransformedData: event
5252
});
5353

5454
// Use Redis LIST for FIFO queue (LPUSH + BRPOP pattern)
5555
const result = await this.client.lPush(queueName, eventJson);
5656

57-
LogEngine.info(`Event successfully queued: ${event.data?.eventId || 'unknown'} -> ${queueName} (${result} items in queue)`);
57+
LogEngine.info(`Event successfully queued: ${event.data?.eventId || 'unknown'} -> ${queueName} (${result} items in queue)`);
5858
return result;
5959
} catch (err) {
6060
LogEngine.error(`Error publishing event to queue: ${err}`);
6161
throw err;
6262
}
6363
}
6464

65+
/**
66+
* Check if a key exists in Redis
67+
* @param prefix - Key prefix (e.g., eventId or fingerprint prefix)
68+
* @param id - Unique identifier to check
69+
* @returns Promise<boolean> - true if key exists
70+
*/
71+
private async keyExists(prefix: string, id: string): Promise<boolean> {
72+
const key = `${prefix}${id}`;
73+
const exists = await this.client.exists(key);
74+
return exists === 1;
75+
}
76+
77+
/**
78+
* Mark a key as processed with automatic expiration
79+
* @param prefix - Key prefix (e.g., eventId or fingerprint prefix)
80+
* @param id - Unique identifier to mark
81+
* @param ttlSeconds - Time to live in seconds (default: 3 days from config)
82+
*/
83+
private async markKey(prefix: string, id: string, ttlSeconds?: number): Promise<void> {
84+
const key = `${prefix}${id}`;
85+
const ttl = ttlSeconds || redisEventConfig.eventTtl;
86+
await this.client.setEx(key, ttl, 'processed');
87+
}
88+
6589
/**
6690
* Check if webhook event already exists (duplicate detection)
6791
* @param eventId - Unique event identifier
6892
* @returns Promise<boolean> - true if event exists
6993
*/
7094
async eventExists(eventId: string): Promise<boolean> {
71-
const key = `${redisEventConfig.keyPrefix}${eventId}`;
72-
const exists = await this.client.exists(key);
73-
return exists === 1;
95+
return this.keyExists(redisEventConfig.keyPrefix, eventId);
7496
}
7597

7698
/**
@@ -79,9 +101,23 @@ export class RedisService {
79101
* @param ttlSeconds - Time to live in seconds (default: 3 days from config)
80102
*/
81103
async markEventProcessed(eventId: string, ttlSeconds?: number): Promise<void> {
82-
const key = `${redisEventConfig.keyPrefix}${eventId}`;
83-
const ttl = ttlSeconds || redisEventConfig.eventTtl; // 3 days default
84-
await this.client.setEx(key, ttl, 'processed');
104+
return this.markKey(redisEventConfig.keyPrefix, eventId, ttlSeconds);
105+
}
106+
107+
/**
108+
* Atomically claim a composite fingerprint slot (SET NX pattern).
109+
* Combines existence check and marking into a single atomic Redis operation,
110+
* eliminating the race window between check and mark.
111+
*
112+
* @param fingerprint - Composite key: eventTimestamp:event:data.id
113+
* @param ttlSeconds - Time to live in seconds (default: 3 days from config)
114+
* @returns Promise<boolean> - true if claimed (new), false if already exists (duplicate)
115+
*/
116+
async claimFingerprint(fingerprint: string, ttlSeconds?: number): Promise<boolean> {
117+
const key = `${redisEventConfig.fingerprintPrefix}${fingerprint}`;
118+
const ttl = ttlSeconds || redisEventConfig.eventTtl;
119+
const result = await this.client.set(key, 'processed', { EX: ttl, NX: true });
120+
return result !== null;
85121
}
86122

87123
async close(): Promise<void> {

0 commit comments

Comments
 (0)