Skip to content

Commit 9a7858c

Browse files
authored
chore: add OHLCVService for real-time candlestick WebSocket streaming (#8695)
## Explanation ### Architecture Overview ``` ┌─────────────────┐ messenger ┌──────────────────────────┐ │ OHLCVService │ ─── calls actions ────► │ BackendWebSocketService │ │ (domain logic) │ │ (raw WS connection) │ │ │ ◄── listens to events ── │ │ └────────┬────────┘ └──────────┬───────────────┘ │ │ publishes events actual WebSocket to UI consumers (connect, auth, reconnect, heartbeat, JSON framing) │ ▼ ┌──────────────────┐ │ Mobile UI │ │ (React hooks) │ │ useOHLCVRealtime │ └──────────────────┘ ``` ### What - Add `OHLCVService` for real-time OHLCV (candlestick) data streaming via the backend WebSocket gateway - Move all WebSocket-related files (`BackendWebSocketService`, `AccountActivityService`) into a new `src/ws/` directory per code review feedback ### Why - Enable real-time chart updates on the Token Details screen without polling - Reduce API load by replacing periodic HTTP calls with persistent WebSocket subscriptions - Organize WebSocket code into a dedicated `ws/` folder for better discoverability ### New files - `src/ws/ohlcv/OHLCVService.ts` — main service with subscribe/unsubscribe semantics, reference counting, grace-period unsubscribe, idempotency checks, chain-status forwarding, and automatic resubscription on reconnect - `src/ws/ohlcv/OHLCVService.test.ts` — 22 unit tests covering all paths (100% branch coverage) - `src/ws/ohlcv/OHLCVService-method-action-types.ts` — auto-generated messenger action types - `src/ws/ohlcv/types.ts` — `OHLCVBar` and `OHLCVSubscriptionOptions` types - `src/ws/ohlcv/index.ts` — barrel exports ### Modified files - `src/index.ts` — added exports for `OHLCVService`, its types, and allowed actions/events; updated import paths to `./ws/` - `eslint-suppressions.json` — updated paths for moved files, added suppressions for new test file - `CHANGELOG.md` — documented new service and exports ### Moved files (no logic changes) - `src/BackendWebSocketService.ts` → `src/ws/BackendWebSocketService.ts` - `src/BackendWebSocketService.test.ts` → `src/ws/BackendWebSocketService.test.ts` - `src/BackendWebSocketService-method-action-types.ts` → `src/ws/BackendWebSocketService-method-action-types.ts` - `src/AccountActivityService.ts` → `src/ws/AccountActivityService.ts` - `src/AccountActivityService.test.ts` → `src/ws/AccountActivityService.test.ts` - `src/AccountActivityService-method-action-types.ts` → `src/ws/AccountActivityService-method-action-types.ts` - Only import path updates (`./logger` → `../logger`, `./types` → `../types`, test helper paths) ### Key design decisions - **UI-driven lifecycle** — unlike `AccountActivityService` (auto-subscribes on account change), `OHLCVService` exposes `subscribe()`/`unsubscribe()` called by the UI when the chart mounts/unmounts - **Reference counting** — multiple UI consumers subscribing to the same assetId/interval/currency share one WebSocket subscription - **Grace period (3s)** — when all consumers unsubscribe, actual WS unsubscribe is delayed 3 seconds to absorb rapid navigation (Token A → Token B → Token A) - **Idempotency** — uses `channelHasSubscription` before subscribing; duplicate calls are no-ops (React Strict Mode safe) - **Chain status** — listens to `system-notifications.v1.market-data.v1` (auto-subscribed by server) and publishes `OHLCVService:chainStatusChanged` - **Disconnect handling** — on WebSocket disconnect, publishes `chainStatusChanged { status: 'down' }` for all tracked chains, triggering UI polling fallback - **Reconnect** — resubscribes all active channels when WebSocket reconnects (no `sessionId` needed for OHLCV; UI polling fallback covers the gap) - **`init()` method** — system notification callback registered in `init()` (not constructor) to comply with messenger-in-constructor lint rule ### Events published - `OHLCVService:barUpdated` — `{ channel, bar: OHLCVBar }` — new candle data from WebSocket - `OHLCVService:chainStatusChanged` — `{ chainIds, status, timestamp? }` — chain up/down (server notification or WS disconnect) - `OHLCVService:subscriptionError` — `{ channel, error, operation }` — subscribe or unsubscribe failure ## References * Related to https://www.notion.so/metamask-consensys/OHLCV-WebSocket-Integration-UI-Implementation-Guide-346f86d67d6880b6a70fc3be0f0c34b9 * Related to MetaMask/metamask-mobile#29739 * Fixes https://consensyssoftware.atlassian.net/browse/ASSETS-3195 ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a new WebSocket-driven market-data service with reference counting, timers, and reconnect resubscription logic, which can affect subscription lifecycles and event delivery. Also moves existing WebSocket services into `src/ws/`, so consumers relying on internal paths (vs package exports) could break if any remain. > > **Overview** > Adds a new `OHLCVService` to stream real-time OHLCV bars over WebSocket, exposing `subscribe`/`unsubscribe` via messenger actions, publishing `barUpdated`/`chainStatusChanged`/`subscriptionError` events, and handling reconnect resubscription with ref-counting plus a grace-period unsubscribe (mutex-protected). > > Refactors `core-backend` by moving `BackendWebSocketService` and `AccountActivityService` (and their tests/action-type files) into `src/ws/`, updating imports/exports (`src/index.ts`), and updating lint suppressions; also adds `async-mutex` plus comprehensive unit tests for the new service and documents the addition in the changelog. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 730af62. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 057b1d1 commit 9a7858c

16 files changed

Lines changed: 1835 additions & 25 deletions

eslint-suppressions.json

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -862,34 +862,45 @@
862862
"count": 1
863863
}
864864
},
865-
"packages/core-backend/src/AccountActivityService.test.ts": {
865+
"packages/core-backend/src/api/shared-types.ts": {
866866
"no-restricted-syntax": {
867-
"count": 2
867+
"count": 1
868868
}
869869
},
870-
"packages/core-backend/src/AccountActivityService.ts": {
870+
"packages/core-backend/src/index.ts": {
871871
"no-restricted-syntax": {
872-
"count": 1
872+
"count": 4
873873
}
874874
},
875-
"packages/core-backend/src/BackendWebSocketService.test.ts": {
875+
"packages/core-backend/src/ws/AccountActivityService.test.ts": {
876876
"no-restricted-syntax": {
877-
"count": 1
877+
"count": 2
878878
}
879879
},
880-
"packages/core-backend/src/BackendWebSocketService.ts": {
880+
"packages/core-backend/src/ws/AccountActivityService.ts": {
881881
"no-restricted-syntax": {
882-
"count": 5
882+
"count": 1
883883
}
884884
},
885-
"packages/core-backend/src/api/shared-types.ts": {
885+
"packages/core-backend/src/ws/BackendWebSocketService.test.ts": {
886886
"no-restricted-syntax": {
887887
"count": 1
888888
}
889889
},
890-
"packages/core-backend/src/index.ts": {
890+
"packages/core-backend/src/ws/BackendWebSocketService.ts": {
891891
"no-restricted-syntax": {
892+
"count": 5
893+
}
894+
},
895+
"packages/core-backend/src/ws/ohlcv/OHLCVService.test.ts": {
896+
"@typescript-eslint/naming-convention": {
892897
"count": 2
898+
},
899+
"id-length": {
900+
"count": 1
901+
},
902+
"no-restricted-syntax": {
903+
"count": 1
893904
}
894905
},
895906
"packages/delegation-controller/src/DelegationController.test.ts": {

packages/core-backend/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `OHLCVService` for real-time OHLCV (candlestick) data streaming via WebSocket ([#8695](https://github.com/MetaMask/core/pull/8695))
13+
- Wraps `BackendWebSocketService` through the messenger pattern to provide subscribe/unsubscribe semantics for market-data OHLCV channels
14+
- Includes reference counting, grace-period unsubscribe, idempotency checks, chain-status forwarding, and automatic resubscription on reconnect
15+
- Export new types `OHLCVBar`, `OHLCVSubscriptionOptions`, `OHLCVSystemNotificationData`, `OHLCVServiceOptions`, `OHLCVServiceActions`, `OHLCVServiceAllowedActions`, `OHLCVServiceBarUpdatedEvent`, `OHLCVServiceChainStatusChangedEvent`, `OHLCVServiceSubscriptionErrorEvent`, `OHLCVServiceEvents`, `OHLCVServiceAllowedEvents`, and `OHLCVServiceMessenger` ([#8695](https://github.com/MetaMask/core/pull/8695))
16+
- Export new constants `OHLCV_SERVICE_ALLOWED_ACTIONS` and `OHLCV_SERVICE_ALLOWED_EVENTS` for configuring the messenger ([#8695](https://github.com/MetaMask/core/pull/8695))
17+
1018
### Changed
1119

1220
- Bump `@metamask/accounts-controller` from `^38.1.0` to `^38.1.1` ([#8774](https://github.com/MetaMask/core/pull/8774))

packages/core-backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@metamask/profile-sync-controller": "^28.1.0",
6161
"@metamask/utils": "^11.9.0",
6262
"@tanstack/query-core": "^5.62.16",
63+
"async-mutex": "^0.5.0",
6364
"uuid": "^8.3.2"
6465
},
6566
"devDependencies": {

packages/core-backend/src/index.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export {
99
getCloseReason,
1010
WebSocketState,
1111
WebSocketEventType,
12-
} from './BackendWebSocketService';
12+
} from './ws/BackendWebSocketService';
1313

1414
export type {
1515
BackendWebSocketServiceOptions,
@@ -24,7 +24,7 @@ export type {
2424
BackendWebSocketServiceConnectionStateChangedEvent,
2525
BackendWebSocketServiceEvents,
2626
BackendWebSocketServiceMessenger,
27-
} from './BackendWebSocketService';
27+
} from './ws/BackendWebSocketService';
2828

2929
// ============================================================================
3030
// ACCOUNT ACTIVITY SERVICE
@@ -34,7 +34,7 @@ export {
3434
AccountActivityService,
3535
ACCOUNT_ACTIVITY_SERVICE_ALLOWED_ACTIONS,
3636
ACCOUNT_ACTIVITY_SERVICE_ALLOWED_EVENTS,
37-
} from './AccountActivityService';
37+
} from './ws/AccountActivityService';
3838

3939
export type {
4040
SystemNotificationData,
@@ -49,7 +49,7 @@ export type {
4949
AccountActivityServiceEvents,
5050
AllowedEvents as AccountActivityServiceAllowedEvents,
5151
AccountActivityServiceMessenger,
52-
} from './AccountActivityService';
52+
} from './ws/AccountActivityService';
5353

5454
// ============================================================================
5555
// SHARED TYPES
@@ -80,6 +80,31 @@ export type {
8080
ApiPlatformClientServiceMessenger,
8181
} from './ApiPlatformClientService';
8282

83+
// ============================================================================
84+
// OHLCV SERVICE
85+
// ============================================================================
86+
87+
export {
88+
OHLCVService,
89+
OHLCV_SERVICE_ALLOWED_ACTIONS,
90+
OHLCV_SERVICE_ALLOWED_EVENTS,
91+
} from './ws/ohlcv';
92+
93+
export type {
94+
OHLCVBar,
95+
OHLCVSubscriptionOptions,
96+
OHLCVSystemNotificationData,
97+
OHLCVServiceOptions,
98+
OHLCVServiceActions,
99+
OHLCVServiceAllowedActions,
100+
OHLCVServiceBarUpdatedEvent,
101+
OHLCVServiceChainStatusChangedEvent,
102+
OHLCVServiceSubscriptionErrorEvent,
103+
OHLCVServiceEvents,
104+
OHLCVServiceAllowedEvents,
105+
OHLCVServiceMessenger,
106+
} from './ws/ohlcv';
107+
83108
// ============================================================================
84109
// API PLATFORM CLIENT
85110
// ============================================================================

packages/core-backend/src/AccountActivityService-method-action-types.ts renamed to packages/core-backend/src/ws/AccountActivityService-method-action-types.ts

File renamed without changes.

packages/core-backend/src/AccountActivityService.test.ts renamed to packages/core-backend/src/ws/AccountActivityService.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ import type {
77
} from '@metamask/messenger';
88
import type { Hex } from '@metamask/utils';
99

10-
import { flushPromises } from '../../../tests/helpers';
10+
import { flushPromises } from '../../../../tests/helpers';
11+
import type { Transaction, BalanceUpdate } from '../types';
12+
import type { AccountActivityMessage } from '../types';
1113
import { AccountActivityService } from './AccountActivityService';
1214
import type {
1315
AccountActivityServiceMessenger,
1416
SubscriptionOptions,
1517
} from './AccountActivityService';
1618
import type { ServerNotificationMessage } from './BackendWebSocketService';
1719
import { WebSocketState } from './BackendWebSocketService';
18-
import type { Transaction, BalanceUpdate } from './types';
19-
import type { AccountActivityMessage } from './types';
2020

2121
type AllAccountActivityServiceActions =
2222
MessengerActions<AccountActivityServiceMessenger>;

packages/core-backend/src/AccountActivityService.ts renamed to packages/core-backend/src/ws/AccountActivityService.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import type { TraceCallback } from '@metamask/controller-utils';
1313
import type { InternalAccount } from '@metamask/keyring-internal-api';
1414
import type { Messenger } from '@metamask/messenger';
1515

16+
import { projectLogger, createModuleLogger } from '../logger';
17+
import type {
18+
Transaction,
19+
AccountActivityMessage,
20+
BalanceUpdate,
21+
} from '../types';
1622
import type { AccountActivityServiceMethodActions } from './AccountActivityService-method-action-types';
1723
import type {
1824
WebSocketConnectionInfo,
@@ -21,12 +27,6 @@ import type {
2127
} from './BackendWebSocketService';
2228
import { WebSocketState } from './BackendWebSocketService';
2329
import type { BackendWebSocketServiceMethodActions } from './BackendWebSocketService-method-action-types';
24-
import { projectLogger, createModuleLogger } from './logger';
25-
import type {
26-
Transaction,
27-
AccountActivityMessage,
28-
BalanceUpdate,
29-
} from './types';
3030

3131
// =============================================================================
3232
// Types and Constants

packages/core-backend/src/BackendWebSocketService-method-action-types.ts renamed to packages/core-backend/src/ws/BackendWebSocketService-method-action-types.ts

File renamed without changes.

packages/core-backend/src/BackendWebSocketService.test.ts renamed to packages/core-backend/src/ws/BackendWebSocketService.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
MockAnyNamespace,
66
} from '@metamask/messenger';
77

8-
import { flushPromises } from '../../../tests/helpers';
8+
import { flushPromises } from '../../../../tests/helpers';
99
import {
1010
BackendWebSocketService,
1111
getCloseReason,

packages/core-backend/src/BackendWebSocketService.ts renamed to packages/core-backend/src/ws/BackendWebSocketService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import type { AuthenticationController } from '@metamask/profile-sync-controller
99
import { getErrorMessage } from '@metamask/utils';
1010
import { v4 as uuidV4 } from 'uuid';
1111

12+
import { projectLogger, createModuleLogger } from '../logger';
1213
import type { BackendWebSocketServiceMethodActions } from './BackendWebSocketService-method-action-types';
13-
import { projectLogger, createModuleLogger } from './logger';
1414

1515
const SERVICE_NAME = 'BackendWebSocketService' as const;
1616

0 commit comments

Comments
 (0)