Skip to content

Commit d7f51ee

Browse files
authored
Merge pull request #65 from anoncon/feat/issue-17-shared-service-singletons
Refactor backend services to shared singleton instances (Closes #17)
2 parents 2737d93 + 3e14c76 commit d7f51ee

File tree

10 files changed

+181
-1713
lines changed

10 files changed

+181
-1713
lines changed

backend/src/api/routes.ts

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

backend/src/index.ts

Lines changed: 115 additions & 356 deletions
Large diffs are not rendered by default.

backend/src/monitoring/rebalancer.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { WebSocketServer } from 'ws'
22
import { StellarService } from '../services/stellar.js'
33
import { ReflectorService } from '../services/reflector.js'
4-
import { RebalanceHistoryService } from '../services/rebalanceHistory.js'
5-
import { RiskManagementService } from '../services/riskManagements.js'
4+
import { rebalanceHistoryService, riskManagementService } from '../services/serviceContainer.js'
65
import { portfolioStorage } from '../services/portfolioStorage.js'
76
import { getPortfolioCheckQueue } from '../queue/queues.js'
87
import { logger } from '../utils/logger.js'
@@ -11,15 +10,11 @@ import type { Portfolio, RiskAlert } from '../types/index.js'
1110
export class RebalancingService {
1211
private stellarService: StellarService
1312
private reflectorService: ReflectorService
14-
private rebalanceHistoryService: RebalanceHistoryService
15-
private riskManagementService: RiskManagementService
1613
private wss: WebSocketServer
1714

1815
constructor(wss: WebSocketServer) {
1916
this.stellarService = new StellarService()
2017
this.reflectorService = new ReflectorService()
21-
this.rebalanceHistoryService = new RebalanceHistoryService()
22-
this.riskManagementService = new RiskManagementService()
2318
this.wss = wss
2419
}
2520

@@ -51,8 +46,8 @@ export class RebalancingService {
5146
}
5247

5348
async getStatus(): Promise<any> {
54-
const stats = await this.rebalanceHistoryService.getHistoryStats()
55-
const circuitBreakers = this.riskManagementService.getCircuitBreakerStatus()
49+
const stats = await rebalanceHistoryService.getHistoryStats()
50+
const circuitBreakers = riskManagementService.getCircuitBreakerStatus()
5651
const active = await this.getActivePortfolios()
5752
return {
5853
activePortfolios: active.length,
@@ -69,12 +64,12 @@ export class RebalancingService {
6964
const prices = await this.reflectorService.getCurrentPrices()
7065
const portfolio = await this.stellarService.getPortfolio(portfolioId)
7166

72-
const riskAlerts = this.riskManagementService.updatePriceData(prices)
67+
const riskAlerts = riskManagementService.updatePriceData(prices)
7368
const needsRebalance = await this.stellarService.checkRebalanceNeeded(portfolioId)
7469

7570
if (needsRebalance) {
7671
logger.info(`Portfolio ${portfolioId} needs rebalancing – enqueueing job`)
77-
const riskCheck = this.riskManagementService.shouldAllowRebalance(portfolio, prices)
72+
const riskCheck = riskManagementService.shouldAllowRebalance(portfolio, prices)
7873

7974
if (riskCheck.allowed) {
8075
// Enqueue a rebalance job rather than executing inline
@@ -97,7 +92,7 @@ export class RebalancingService {
9792
alerts: riskCheck.alerts,
9893
})
9994

100-
await this.rebalanceHistoryService.recordRebalanceEvent({
95+
await rebalanceHistoryService.recordRebalanceEvent({
10196
portfolioId,
10297
trigger: 'Automatic Check – Blocked',
10398
trades: 0,

backend/src/queue/workers/portfolioCheckWorker.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { Worker, Job } from 'bullmq'
22
import { getConnectionOptions } from '../connection.js'
33
import { StellarService } from '../../services/stellar.js'
44
import { ReflectorService } from '../../services/reflector.js'
5-
import { RebalanceHistoryService } from '../../services/rebalanceHistory.js'
6-
import { RiskManagementService } from '../../services/riskManagements.js'
5+
import { riskManagementService } from '../../services/serviceContainer.js'
76
import { portfolioStorage } from '../../services/portfolioStorage.js'
87
import { CircuitBreakers } from '../../services/circuitBreakers.js'
98
import { notificationService } from '../../services/notificationService.js'
@@ -27,7 +26,6 @@ export async function processPortfolioCheckJob(
2726

2827
const stellarService = new StellarService()
2928
const reflectorService = new ReflectorService()
30-
const riskManagementService = new RiskManagementService()
3129

3230
const allPortfolios = await portfolioStorage.getAllPortfolios()
3331

backend/src/queue/workers/rebalanceWorker.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Worker, Job } from 'bullmq'
22
import { getConnectionOptions } from '../connection.js'
33
import { StellarService } from '../../services/stellar.js'
4-
import { RebalanceHistoryService } from '../../services/rebalanceHistory.js'
4+
import { rebalanceHistoryService } from '../../services/serviceContainer.js'
55
import { notificationService } from '../../services/notificationService.js'
66
import { logger } from '../../utils/logger.js'
77
import type { RebalanceJobData } from '../queues.js'
@@ -24,8 +24,6 @@ export async function processRebalanceJob(
2424
})
2525

2626
const stellarService = new StellarService()
27-
const rebalanceHistoryService = new RebalanceHistoryService()
28-
2927
try {
3028
const portfolio = await stellarService.getPortfolio(portfolioId)
3129
const rebalanceResult = await stellarService.executeRebalance(portfolioId)

backend/src/services/autoRebalancer.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,27 @@
1-
import { StellarService } from './stellar.js'
2-
import { ReflectorService } from './reflector.js'
3-
import { RebalanceHistoryService } from './rebalanceHistory.js'
4-
import { RiskManagementService } from './riskManagements.js'
5-
import { portfolioStorage } from './portfolioStorage.js'
1+
import { StellarService } from './stellar.js'
2+
import { ReflectorService } from './reflector.js'
3+
import { rebalanceHistoryService } from './serviceContainer.js'
4+
import { portfolioStorage } from './portfolioStorage.js'
65
import { CircuitBreakers } from './circuitBreakers.js'
76
import { notificationService } from './notificationService.js'
87
import { logger } from '../utils/logger.js'
98
import { getPortfolioCheckQueue } from '../queue/queues.js'
109
import { isRedisAvailable } from '../queue/connection.js'
1110

12-
export class AutoRebalancerService {
13-
private stellarService: StellarService
14-
private reflectorService: ReflectorService
15-
private rebalanceHistoryService: RebalanceHistoryService
16-
private riskManagementService: RiskManagementService
17-
private isRunning = false
11+
export class AutoRebalancerService {
12+
private stellarService: StellarService
13+
private reflectorService: ReflectorService
14+
private isRunning = false
1815

1916
// Configuration (kept for getStatus() compatibility)
2017
private readonly CHECK_INTERVAL = 30 * 60 * 1000 // 30 minutes
2118
private readonly MIN_REBALANCE_INTERVAL = 24 * 60 * 60 * 1000
2219
private readonly MAX_AUTO_REBALANCES_PER_DAY = 3
2320

24-
constructor() {
25-
this.stellarService = new StellarService()
26-
this.reflectorService = new ReflectorService()
27-
this.rebalanceHistoryService = new RebalanceHistoryService()
28-
this.riskManagementService = new RiskManagementService()
29-
}
21+
constructor() {
22+
this.stellarService = new StellarService()
23+
this.reflectorService = new ReflectorService()
24+
}
3025

3126
/**
3227
* Start the automatic monitoring service.
@@ -112,7 +107,7 @@ export class AutoRebalancerService {
112107
averageRebalancesPerDay: number
113108
}> {
114109
try {
115-
const allAutoRebalances = await this.rebalanceHistoryService.getAllAutoRebalances()
110+
const allAutoRebalances = await rebalanceHistoryService.getAllAutoRebalances()
116111

117112
const today = new Date()
118113
today.setHours(0, 0, 0, 0)

backend/src/services/rebalanceHistory.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ export interface RebalanceEvent {
3636
}
3737
}
3838

39-
export class RebalanceHistoryService {
40-
private riskService: RiskManagementService
41-
42-
constructor() {
43-
this.riskService = new RiskManagementService()
44-
}
39+
export class RebalanceHistoryService {
40+
private riskService: RiskManagementService
41+
42+
constructor(riskService?: RiskManagementService) {
43+
this.riskService = riskService ?? new RiskManagementService()
44+
}
4545

4646
async recordRebalanceEvent(eventData: {
4747
portfolioId: string
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { RebalanceHistoryService } from './rebalanceHistory.js'
2+
import { RiskManagementService } from './riskManagements.js'
3+
4+
const riskManagementService = new RiskManagementService()
5+
const rebalanceHistoryService = new RebalanceHistoryService(riskManagementService)
6+
7+
export {
8+
riskManagementService,
9+
rebalanceHistoryService
10+
}

backend/src/services/stellar.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type RebalanceExecutionConfig
1010
} from './dex.js'
1111
import { getFeatureFlags } from '../config/featureFlags.js'
12+
import { rebalanceHistoryService, riskManagementService } from './serviceContainer.js'
1213

1314
interface StoredPortfolio {
1415
id: string
@@ -195,22 +196,18 @@ export class StellarService {
195196
const { portfolioStorage } = await import('./portfolioStorage.js')
196197
const { CircuitBreakers } = await import('./circuitBreakers.js')
197198
const { ReflectorService } = await import('./reflector.js')
198-
const { RebalanceHistoryService } = await import('./rebalanceHistory.js')
199-
const { RiskManagementService } = await import('./riskManagements.js')
200199

201200
const portfolio = await portfolioStorage.getPortfolio(portfolioId) as StoredPortfolio | undefined
202201
if (!portfolio) {
203202
throw new Error('Portfolio not found')
204203
}
205204

206205
const reflector = new ReflectorService()
207-
const rebalanceHistory = new RebalanceHistoryService()
208-
const riskService = new RiskManagementService()
209206
const prices = await reflector.getCurrentPrices()
210207

211-
const riskCheck = riskService.shouldAllowRebalance(portfolio, prices)
208+
const riskCheck = riskManagementService.shouldAllowRebalance(portfolio, prices)
212209
if (!riskCheck.allowed) {
213-
await rebalanceHistory.recordRebalanceEvent({
210+
await rebalanceHistoryService.recordRebalanceEvent({
214211
portfolioId,
215212
trigger: 'Risk Management Block',
216213
trades: 0,
@@ -228,7 +225,7 @@ export class StellarService {
228225
const hourInMs = 60 * 60 * 1000
229226

230227
if (now - lastRebalance < hourInMs) {
231-
await rebalanceHistory.recordRebalanceEvent({
228+
await rebalanceHistoryService.recordRebalanceEvent({
232229
portfolioId,
233230
trigger: 'Cooldown Period Active',
234231
trades: 0,
@@ -243,7 +240,7 @@ export class StellarService {
243240

244241
const marketCheck = await CircuitBreakers.checkMarketConditions(prices)
245242
if (!marketCheck.safe) {
246-
await rebalanceHistory.recordRebalanceEvent({
243+
await rebalanceHistoryService.recordRebalanceEvent({
247244
portfolioId,
248245
trigger: 'Circuit Breaker Triggered',
249246
trades: 0,
@@ -258,7 +255,7 @@ export class StellarService {
258255

259256
const needed = await this.checkRebalanceNeeded(portfolioId)
260257
if (!needed) {
261-
await rebalanceHistory.recordRebalanceEvent({
258+
await rebalanceHistoryService.recordRebalanceEvent({
262259
portfolioId,
263260
trigger: 'No Rebalance Needed',
264261
trades: 0,
@@ -276,7 +273,7 @@ export class StellarService {
276273
throw new Error('No executable trades generated from current drift')
277274
}
278275

279-
await rebalanceHistory.recordRebalanceEvent({
276+
await rebalanceHistoryService.recordRebalanceEvent({
280277
portfolioId,
281278
trigger: 'Rebalance Started',
282279
trades: 0,
@@ -323,7 +320,7 @@ export class StellarService {
323320
const failureReasons = dexResult.failedTrades.map(t => t.failureReason).filter(Boolean) as string[]
324321
const historyStatus = dexResult.status === 'failed' ? 'failed' : 'completed'
325322

326-
const event = await rebalanceHistory.recordRebalanceEvent({
323+
const event = await rebalanceHistoryService.recordRebalanceEvent({
327324
portfolioId,
328325
trigger: dexResult.status === 'failed'
329326
? `Execution Failed: ${dexResult.failureReason || 'unknown reason'}`
@@ -362,15 +359,10 @@ export class StellarService {
362359
totalSlippageBps: dexResult.totalSlippageBps
363360
}
364361
} catch (error) {
365-
// Bubble up concurrency conflicts without wrapping so callers can
366-
// distinguish a 409 Conflict from a generic 500 failure.
367-
if (error instanceof ConflictError) throw error
368362

369-
const { RebalanceHistoryService } = await import('./rebalanceHistory.js')
370-
const rebalanceHistory = new RebalanceHistoryService()
371363
const message = error instanceof Error ? error.message : String(error)
372364

373-
await rebalanceHistory.recordRebalanceEvent({
365+
await rebalanceHistoryService.recordRebalanceEvent({
374366
portfolioId,
375367
trigger: 'Execution Failed',
376368
trades: 0,

backend/src/test/queue.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@ vi.mock('../services/riskManagements.js', () => {
5959
return { RiskManagementService }
6060
})
6161

62+
vi.mock('../services/serviceContainer.js', () => ({
63+
rebalanceHistoryService: {
64+
recordRebalanceEvent: vi.fn().mockResolvedValue({ id: 'hist-1' }),
65+
getRecentAutoRebalances: vi.fn().mockResolvedValue([]),
66+
getAutoRebalancesSince: vi.fn().mockResolvedValue([]),
67+
getAllAutoRebalances: vi.fn().mockResolvedValue([]),
68+
getHistoryStats: vi.fn().mockResolvedValue({
69+
totalEvents: 0,
70+
portfolios: 0,
71+
recentActivity: 0,
72+
autoRebalances: 0
73+
})
74+
},
75+
riskManagementService: {
76+
shouldAllowRebalance: vi.fn().mockReturnValue({ allowed: true, reason: 'OK', alerts: [] }),
77+
updatePriceData: vi.fn().mockReturnValue([]),
78+
getCircuitBreakerStatus: vi.fn().mockReturnValue({})
79+
}
80+
}))
81+
6282
vi.mock('../services/portfolioStorage.js', () => ({
6383
portfolioStorage: {
6484
getAllPortfolios: vi.fn().mockResolvedValue([{ id: 'test-portfolio-1', threshold: 5 }]),

0 commit comments

Comments
 (0)