Skip to content

Commit a453805

Browse files
Add SinkingYachts realtime feed integration
- Enhanced SinkingYahtsService with WebSocket support for real-time updates - Added bulk import and incremental update capabilities via REST API - Implemented automatic reconnection with exponential backoff - Added admin management endpoints for feed control - Integrated auto-start configuration with environment variable controls - Added ws and @types/ws dependencies for WebSocket functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Jasper Mayone <[email protected]>
1 parent 10b7549 commit a453805

File tree

5 files changed

+456
-2
lines changed

5 files changed

+456
-2
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
"request-ip": "^3.3.0",
4848
"resend": "^4.2.0",
4949
"response-time": "^2.3.3",
50-
"ts-node": "^10.9.2"
50+
"ts-node": "^10.9.2",
51+
"ws": "^8.18.0"
5152
},
5253
"devDependencies": {
5354
"@types/react": "^19.0.12",
@@ -60,6 +61,7 @@
6061
"@types/node-statsd": "^0.1.6",
6162
"@types/request-ip": "^0.0.41",
6263
"@types/response-time": "^2.3.8",
64+
"@types/ws": "^8.5.13",
6365
"nodemon": "^3.1.9",
6466
"typescript": "^5.7.2"
6567
},

src/admin-routes/router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { logRequest } from "src/middleware/logRequest";
1414
import { db } from "src/utils/db";
1515
import { authenticateToken, getUserInfo } from "src/utils/jwt";
1616
import domainRouter from "./routes/domain";
17+
import sinkingYachtsRouter from "./routes/sinking-yachts";
1718
import userRouter from "./routes/user";
1819
const router = express.Router();
1920
router.use(express.json());
@@ -297,6 +298,7 @@ router.get("/metrics", logRequest, async (req, res) => {
297298
});
298299

299300
router.use("/domain", logRequest, domainRouter);
301+
router.use("/sinking-yachts", logRequest, sinkingYachtsRouter);
300302
router.use("/user", userRouter);
301303

302304
export default router;
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import express, { Request, Response } from "express";
2+
import { sinkingYahtsService } from "src/services/_index";
3+
4+
const router = express.Router();
5+
6+
/**
7+
* POST /admin/sinking-yachts/start-feed
8+
* @summary Starts the SinkingYachts realtime feed monitoring
9+
* @tags SinkingYachts - Feed Management
10+
* @security BearerAuth
11+
* @param {object} request.body
12+
* @param {boolean} request.body.skipBulkImport - Skip the initial bulk import (optional)
13+
* @return {object} 200 - Success response
14+
* @return {object} 500 - Error response
15+
*/
16+
router.post("/start-feed", async (req: Request, res: Response) => {
17+
try {
18+
const { skipBulkImport = false } = req.body;
19+
20+
await sinkingYahtsService.startFeedMonitoring(skipBulkImport);
21+
22+
return res.status(200).json({
23+
success: true,
24+
message: "SinkingYachts feed monitoring started successfully",
25+
skipBulkImport
26+
});
27+
} catch (error: any) {
28+
return res.status(500).json({
29+
success: false,
30+
message: "Failed to start SinkingYachts feed monitoring",
31+
error: error.message
32+
});
33+
}
34+
});
35+
36+
/**
37+
* POST /admin/sinking-yachts/stop-feed
38+
* @summary Stops the SinkingYachts realtime feed monitoring
39+
* @tags SinkingYachts - Feed Management
40+
* @security BearerAuth
41+
* @return {object} 200 - Success response
42+
*/
43+
router.post("/stop-feed", async (req: Request, res: Response) => {
44+
try {
45+
sinkingYahtsService.stopRealtimeFeed();
46+
47+
return res.status(200).json({
48+
success: true,
49+
message: "SinkingYachts feed monitoring stopped successfully"
50+
});
51+
} catch (error: any) {
52+
return res.status(500).json({
53+
success: false,
54+
message: "Failed to stop SinkingYachts feed monitoring",
55+
error: error.message
56+
});
57+
}
58+
});
59+
60+
/**
61+
* POST /admin/sinking-yachts/bulk-import
62+
* @summary Manually triggers a bulk import of all domains from SinkingYachts
63+
* @tags SinkingYachts - Feed Management
64+
* @security BearerAuth
65+
* @return {object} 200 - Success response
66+
* @return {object} 500 - Error response
67+
*/
68+
router.post("/bulk-import", async (req: Request, res: Response) => {
69+
try {
70+
await sinkingYahtsService.initializeBulkImport();
71+
72+
return res.status(200).json({
73+
success: true,
74+
message: "SinkingYachts bulk import completed successfully"
75+
});
76+
} catch (error: any) {
77+
return res.status(500).json({
78+
success: false,
79+
message: "Failed to complete SinkingYachts bulk import",
80+
error: error.message
81+
});
82+
}
83+
});
84+
85+
/**
86+
* GET /admin/sinking-yachts/recent
87+
* @summary Fetches recent domains from SinkingYachts API
88+
* @tags SinkingYachts - Feed Management
89+
* @security BearerAuth
90+
* @param {string} since.query.required - ISO date string for filtering recent domains
91+
* @return {object} 200 - Array of recent domains
92+
* @return {object} 400 - Bad request (missing since parameter)
93+
* @return {object} 500 - Error response
94+
*/
95+
router.get("/recent", async (req: Request, res: Response) => {
96+
try {
97+
const { since } = req.query;
98+
99+
if (!since || typeof since !== "string") {
100+
return res.status(400).json({
101+
success: false,
102+
message: "Missing or invalid 'since' query parameter (ISO date string required)"
103+
});
104+
}
105+
106+
const recentDomains = await sinkingYahtsService.getRecentDomains(since);
107+
108+
return res.status(200).json({
109+
success: true,
110+
data: recentDomains,
111+
count: recentDomains.length,
112+
since
113+
});
114+
} catch (error: any) {
115+
return res.status(500).json({
116+
success: false,
117+
message: "Failed to fetch recent domains",
118+
error: error.message
119+
});
120+
}
121+
});
122+
123+
export default router;

src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { server } from "./server";
1111
// import metrics from "./metrics";
1212
import { swaggerOptions as adminSwagOptions } from "./admin-routes/swaggerOptions";
1313
import { swaggerOptions as mainSwagOptions } from "./swaggerOptions";
14+
import { sinkingYahtsService } from "./services/_index";
1415
import * as logger from "./utils/logger";
1516
dotenv.config();
1617

@@ -26,6 +27,21 @@ try {
2627
}
2728
const db = drizzle(process.env.DATABASE_URL);
2829
logger.database("Database connection initialized successfully");
30+
31+
// Initialize SinkingYachts realtime feed monitoring
32+
if (process.env.ENABLE_SINKING_YACHTS_FEED !== "false") {
33+
logger.info("Starting SinkingYachts realtime feed monitoring...");
34+
sinkingYahtsService.startFeedMonitoring(process.env.SKIP_BULK_IMPORT === "true")
35+
.then(() => {
36+
logger.info("SinkingYachts feed monitoring started successfully");
37+
})
38+
.catch((error) => {
39+
logger.error(`Failed to start SinkingYachts feed monitoring: ${error.message}`);
40+
// Don't exit the process, let the server continue without feed monitoring
41+
});
42+
} else {
43+
logger.info("SinkingYachts feed monitoring disabled by ENABLE_SINKING_YACHTS_FEED=false");
44+
}
2945
} catch (error) {
3046
logger.error(
3147
`Failed to initialize database connection: ${error instanceof Error ? error.message : String(error)}`

0 commit comments

Comments
 (0)