@metamask-previews/core-backend 6.2.2-preview-c7e584108 → 6.2.2-preview-337abd924

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -15,6 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
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
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
17
 
18
+ ### Changed
19
+
20
+ - Bump `@metamask/accounts-controller` from `^38.1.0` to `^38.1.1` ([#8774](https://github.com/MetaMask/core/pull/8774))
21
+ - Bump `@metamask/controller-utils` from `^12.0.0` to `^12.1.0` ([#8774](https://github.com/MetaMask/core/pull/8774))
22
+
18
23
  ## [6.2.2]
19
24
 
20
25
  ### Changed
@@ -18,9 +18,10 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
18
18
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
19
19
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
20
20
  };
21
- var _OHLCVService_instances, _OHLCVService_messenger, _OHLCVService_trace, _OHLCVService_channels, _OHLCVService_channelLocks, _OHLCVService_chainsUp, _OHLCVService_subscribeInner, _OHLCVService_unsubscribeInner, _OHLCVService_performUnsubscribe, _OHLCVService_resubscribeActiveChannels, _OHLCVService_handleBarUpdate, _OHLCVService_handleSystemNotification, _OHLCVService_handleWebSocketStateChange, _OHLCVService_buildChannel, _OHLCVService_withChannelLock, _OHLCVService_forceReconnection;
21
+ var _OHLCVService_instances, _OHLCVService_messenger, _OHLCVService_trace, _OHLCVService_channels, _OHLCVService_mutex, _OHLCVService_chainsUp, _OHLCVService_subscribeInner, _OHLCVService_unsubscribeInner, _OHLCVService_performUnsubscribe, _OHLCVService_resubscribeActiveChannels, _OHLCVService_handleBarUpdate, _OHLCVService_handleSystemNotification, _OHLCVService_handleWebSocketStateChange, _OHLCVService_buildChannel, _OHLCVService_forceReconnection;
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.OHLCVService = exports.OHLCV_SERVICE_ALLOWED_EVENTS = exports.OHLCV_SERVICE_ALLOWED_ACTIONS = void 0;
24
+ const async_mutex_1 = require("async-mutex");
24
25
  const logger_1 = require("../../logger.cjs");
25
26
  const BackendWebSocketService_1 = require("../BackendWebSocketService.cjs");
26
27
  // =============================================================================
@@ -73,7 +74,7 @@ class OHLCVService {
73
74
  _OHLCVService_messenger.set(this, void 0);
74
75
  _OHLCVService_trace.set(this, void 0);
75
76
  _OHLCVService_channels.set(this, new Map());
76
- _OHLCVService_channelLocks.set(this, new Map());
77
+ _OHLCVService_mutex.set(this, new async_mutex_1.Mutex());
77
78
  _OHLCVService_chainsUp.set(this, new Set());
78
79
  __classPrivateFieldSet(this, _OHLCVService_messenger, options.messenger, "f");
79
80
  __classPrivateFieldSet(this, _OHLCVService_trace, options.traceFn ??
@@ -107,7 +108,13 @@ class OHLCVService {
107
108
  */
108
109
  async subscribe(options) {
109
110
  const channel = __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_buildChannel).call(this, options);
110
- return __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_withChannelLock).call(this, channel, () => __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_subscribeInner).call(this, channel));
111
+ const releaseLock = await __classPrivateFieldGet(this, _OHLCVService_mutex, "f").acquire();
112
+ try {
113
+ await __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_subscribeInner).call(this, channel);
114
+ }
115
+ finally {
116
+ releaseLock();
117
+ }
111
118
  }
112
119
  /**
113
120
  * Unsubscribe from an OHLCV channel. Decrements the reference count and,
@@ -119,7 +126,13 @@ class OHLCVService {
119
126
  */
120
127
  async unsubscribe(options) {
121
128
  const channel = __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_buildChannel).call(this, options);
122
- return __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_withChannelLock).call(this, channel, () => __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_unsubscribeInner).call(this, channel));
129
+ const releaseLock = await __classPrivateFieldGet(this, _OHLCVService_mutex, "f").acquire();
130
+ try {
131
+ await __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_unsubscribeInner).call(this, channel);
132
+ }
133
+ finally {
134
+ releaseLock();
135
+ }
123
136
  }
124
137
  // =============================================================================
125
138
  // Public — Cleanup
@@ -134,12 +147,12 @@ class OHLCVService {
134
147
  }
135
148
  }
136
149
  __classPrivateFieldGet(this, _OHLCVService_channels, "f").clear();
137
- __classPrivateFieldGet(this, _OHLCVService_channelLocks, "f").clear();
150
+ __classPrivateFieldGet(this, _OHLCVService_chainsUp, "f").clear();
138
151
  __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:removeChannelCallback', SYSTEM_NOTIFICATIONS_CHANNEL);
139
152
  }
140
153
  }
141
154
  exports.OHLCVService = OHLCVService;
142
- _OHLCVService_messenger = new WeakMap(), _OHLCVService_trace = new WeakMap(), _OHLCVService_channels = new WeakMap(), _OHLCVService_channelLocks = new WeakMap(), _OHLCVService_chainsUp = new WeakMap(), _OHLCVService_instances = new WeakSet(), _OHLCVService_subscribeInner = async function _OHLCVService_subscribeInner(channel) {
155
+ _OHLCVService_messenger = new WeakMap(), _OHLCVService_trace = new WeakMap(), _OHLCVService_channels = new WeakMap(), _OHLCVService_mutex = new WeakMap(), _OHLCVService_chainsUp = new WeakMap(), _OHLCVService_instances = new WeakSet(), _OHLCVService_subscribeInner = async function _OHLCVService_subscribeInner(channel) {
143
156
  const entry = __classPrivateFieldGet(this, _OHLCVService_channels, "f").get(channel);
144
157
  if (entry?.gracePeriodTimer) {
145
158
  clearTimeout(entry.gracePeriodTimer);
@@ -149,25 +162,16 @@ _OHLCVService_messenger = new WeakMap(), _OHLCVService_trace = new WeakMap(), _O
149
162
  channel,
150
163
  refCount: entry.refCount,
151
164
  });
152
- return;
165
+ if (__classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:channelHasSubscription', channel)) {
166
+ return;
167
+ }
168
+ // WS subscription was lost (e.g. after disconnect/reconnect) — fall
169
+ // through to recreate it while keeping the already-bumped refCount.
153
170
  }
154
- if (entry && entry.refCount > 0) {
171
+ else if (entry && entry.refCount > 0) {
155
172
  entry.refCount += 1;
156
173
  return;
157
174
  }
158
- // Flush other channels sitting in grace period to free server-side slots
159
- // and prevent accumulation when switching time ranges rapidly.
160
- for (const [otherChannel, otherEntry] of __classPrivateFieldGet(this, _OHLCVService_channels, "f").entries()) {
161
- if (otherChannel !== channel && otherEntry.gracePeriodTimer) {
162
- clearTimeout(otherEntry.gracePeriodTimer);
163
- otherEntry.gracePeriodTimer = undefined;
164
- log('OHLCV-WS: Flushing grace-period channel before new subscribe', {
165
- flushedChannel: otherChannel,
166
- newChannel: channel,
167
- });
168
- await __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_performUnsubscribe).call(this, otherChannel);
169
- }
170
- }
171
175
  try {
172
176
  await __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:connect');
173
177
  if (__classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:channelHasSubscription', channel)) {
@@ -211,6 +215,7 @@ _OHLCVService_messenger = new WeakMap(), _OHLCVService_trace = new WeakMap(), _O
211
215
  return;
212
216
  }
213
217
  entry.gracePeriodTimer = setTimeout(() => {
218
+ entry.gracePeriodTimer = undefined;
214
219
  __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_performUnsubscribe).call(this, channel).catch(() => {
215
220
  // no-op
216
221
  });
@@ -220,30 +225,41 @@ _OHLCVService_messenger = new WeakMap(), _OHLCVService_trace = new WeakMap(), _O
220
225
  // Private — WebSocket Subscription Helpers
221
226
  // =============================================================================
222
227
  async function _OHLCVService_performUnsubscribe(channel) {
223
- log('OHLCV-WS: Grace period expired performing actual WS unsubscribe', {
224
- channel,
225
- });
226
- __classPrivateFieldGet(this, _OHLCVService_channels, "f").delete(channel);
228
+ const releaseLock = await __classPrivateFieldGet(this, _OHLCVService_mutex, "f").acquire();
227
229
  try {
228
- const subscriptions = __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:getSubscriptionsByChannel', channel);
229
- for (const sub of subscriptions) {
230
- await sub.unsubscribe();
230
+ const entry = __classPrivateFieldGet(this, _OHLCVService_channels, "f").get(channel);
231
+ if (entry && entry.refCount > 0) {
232
+ log('OHLCV-WS: Skipping unsubscribe — new subscriber arrived while queued', { channel, refCount: entry.refCount });
233
+ return;
231
234
  }
232
- log('OHLCV-WS: WS unsubscribe completed', { channel });
233
- }
234
- catch (error) {
235
- log('OHLCV-WS: Unsubscription failed, forcing reconnection', {
236
- channel,
237
- error,
238
- });
239
- __classPrivateFieldGet(this, _OHLCVService_messenger, "f").publish('OHLCVService:subscriptionError', {
235
+ log('OHLCV-WS: Grace period expired — performing actual WS unsubscribe', {
240
236
  channel,
241
- error: String(error),
242
- operation: 'unsubscribe',
243
- });
244
- await __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_forceReconnection).call(this).catch(() => {
245
- // no-op
246
237
  });
238
+ __classPrivateFieldGet(this, _OHLCVService_channels, "f").delete(channel);
239
+ try {
240
+ const subscriptions = __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:getSubscriptionsByChannel', channel);
241
+ for (const sub of subscriptions) {
242
+ await sub.unsubscribe();
243
+ }
244
+ log('OHLCV-WS: WS unsubscribe completed', { channel });
245
+ }
246
+ catch (error) {
247
+ log('OHLCV-WS: Unsubscription failed, forcing reconnection', {
248
+ channel,
249
+ error,
250
+ });
251
+ __classPrivateFieldGet(this, _OHLCVService_messenger, "f").publish('OHLCVService:subscriptionError', {
252
+ channel,
253
+ error: String(error),
254
+ operation: 'unsubscribe',
255
+ });
256
+ await __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_forceReconnection).call(this).catch(() => {
257
+ // no-op
258
+ });
259
+ }
260
+ }
261
+ finally {
262
+ releaseLock();
247
263
  }
248
264
  }, _OHLCVService_resubscribeActiveChannels =
249
265
  /**
@@ -251,34 +267,43 @@ async function _OHLCVService_performUnsubscribe(channel) {
251
267
  * Called when WebSocket transitions to CONNECTED.
252
268
  */
253
269
  async function _OHLCVService_resubscribeActiveChannels() {
254
- const channelCount = __classPrivateFieldGet(this, _OHLCVService_channels, "f").size;
255
- log('OHLCV-WS: Resubscribing active channels after reconnect', {
256
- count: channelCount,
257
- });
258
- for (const [channel, entry] of __classPrivateFieldGet(this, _OHLCVService_channels, "f").entries()) {
259
- if (entry.refCount === 0) {
260
- continue;
261
- }
262
- try {
263
- if (__classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:channelHasSubscription', channel)) {
264
- log('OHLCV-WS: Channel already subscribed on server, skipping resubscribe', {
270
+ const releaseLock = await __classPrivateFieldGet(this, _OHLCVService_mutex, "f").acquire();
271
+ try {
272
+ const channelCount = __classPrivateFieldGet(this, _OHLCVService_channels, "f").size;
273
+ log('OHLCV-WS: Resubscribing active channels after reconnect', {
274
+ count: channelCount,
275
+ });
276
+ for (const [channel, entry] of __classPrivateFieldGet(this, _OHLCVService_channels, "f").entries()) {
277
+ if (entry.refCount === 0) {
278
+ continue;
279
+ }
280
+ try {
281
+ if (__classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:channelHasSubscription', channel)) {
282
+ log('OHLCV-WS: Channel already subscribed on server, skipping resubscribe', {
283
+ channel,
284
+ });
285
+ continue;
286
+ }
287
+ await __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:subscribe', {
288
+ channels: [channel],
289
+ channelType: SUBSCRIPTION_NAMESPACE,
290
+ callback: (notification) => {
291
+ __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_handleBarUpdate).call(this, channel, notification);
292
+ },
293
+ });
294
+ log('OHLCV-WS: Resubscription succeeded', { channel });
295
+ }
296
+ catch (error) {
297
+ log('OHLCV-WS: Resubscription failed for channel', {
265
298
  channel,
299
+ error,
266
300
  });
267
- continue;
268
301
  }
269
- await __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:subscribe', {
270
- channels: [channel],
271
- channelType: SUBSCRIPTION_NAMESPACE,
272
- callback: (notification) => {
273
- __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_handleBarUpdate).call(this, channel, notification);
274
- },
275
- });
276
- log('OHLCV-WS: Resubscription succeeded', { channel });
277
- }
278
- catch (error) {
279
- log('OHLCV-WS: Resubscription failed for channel', { channel, error });
280
302
  }
281
303
  }
304
+ finally {
305
+ releaseLock();
306
+ }
282
307
  }, _OHLCVService_handleBarUpdate = function _OHLCVService_handleBarUpdate(channel, notification) {
283
308
  const bar = notification.data;
284
309
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -336,27 +361,6 @@ async function _OHLCVService_resubscribeActiveChannels() {
336
361
  }
337
362
  }, _OHLCVService_buildChannel = function _OHLCVService_buildChannel(options) {
338
363
  return `${SUBSCRIPTION_NAMESPACE}.${options.assetId}.${options.interval}.${options.currency}`;
339
- }, _OHLCVService_withChannelLock =
340
- /**
341
- * Serialize async operations for the same channel so that concurrent
342
- * subscribe/unsubscribe calls cannot interleave and corrupt refCount.
343
- * Different channels are not blocked by each other.
344
- *
345
- * @param channel - The channel key to lock on.
346
- * @param fn - The async function to execute under the lock.
347
- */
348
- async function _OHLCVService_withChannelLock(channel, fn) {
349
- const prev = __classPrivateFieldGet(this, _OHLCVService_channelLocks, "f").get(channel) ?? Promise.resolve();
350
- const next = prev.then(fn, fn);
351
- __classPrivateFieldGet(this, _OHLCVService_channelLocks, "f").set(channel, next);
352
- try {
353
- await next;
354
- }
355
- finally {
356
- if (__classPrivateFieldGet(this, _OHLCVService_channelLocks, "f").get(channel) === next) {
357
- __classPrivateFieldGet(this, _OHLCVService_channelLocks, "f").delete(channel);
358
- }
359
- }
360
364
  }, _OHLCVService_forceReconnection = async function _OHLCVService_forceReconnection() {
361
365
  log('OHLCV-WS: Forcing WebSocket reconnection');
362
366
  await __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:forceReconnection');
@@ -1 +1 @@
1
- {"version":3,"file":"OHLCVService.cjs","sourceRoot":"","sources":["../../../src/ws/ohlcv/OHLCVService.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OHLCV Service for real-time candlestick data streaming via WebSocket.\n *\n * Wraps {@link BackendWebSocketService} through the messenger pattern to\n * provide subscribe/unsubscribe semantics for OHLCV market-data channels.\n * Includes reference counting, grace-period unsubscribe, idempotency checks,\n * chain-status forwarding, and automatic resubscription on reconnect.\n */\n\nimport type {\n TraceCallback,\n TraceContext,\n TraceRequest,\n} from '@metamask/controller-utils';\nimport type { Messenger } from '@metamask/messenger';\n\nimport { projectLogger, createModuleLogger } from '../../logger';\nimport type {\n WebSocketConnectionInfo,\n BackendWebSocketServiceConnectionStateChangedEvent,\n ServerNotificationMessage,\n} from '../BackendWebSocketService';\nimport { WebSocketState } from '../BackendWebSocketService';\nimport type { BackendWebSocketServiceMethodActions } from '../BackendWebSocketService-method-action-types';\nimport type { OHLCVServiceMethodActions } from './OHLCVService-method-action-types';\nimport type { OHLCVBar, OHLCVSubscriptionOptions } from './types';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst SERVICE_NAME = 'OHLCVService';\n\nconst log = createModuleLogger(projectLogger, SERVICE_NAME);\n\nconst MESSENGER_EXPOSED_METHODS = ['subscribe', 'unsubscribe'] as const;\n\nconst SUBSCRIPTION_NAMESPACE = 'market-data.v1';\n\nconst SYSTEM_NOTIFICATIONS_CHANNEL = `system-notifications.v1.${SUBSCRIPTION_NAMESPACE}`;\n\n/** Delay before actually unsubscribing from a channel after refCount reaches 0. */\nconst GRACE_PERIOD_MS = 3_000;\n\n// =============================================================================\n// Types — Channel Tracking\n// =============================================================================\n\ntype ChannelEntry = {\n refCount: number;\n gracePeriodTimer?: ReturnType<typeof setTimeout>;\n};\n\n// =============================================================================\n// Types — System Notifications\n// =============================================================================\n\n/**\n * System notification data for chain status updates on market-data channels.\n */\nexport type OHLCVSystemNotificationData = {\n chainIds: string[];\n status: 'down' | 'up';\n timestamp?: number;\n};\n\n// =============================================================================\n// Types — Service Options\n// =============================================================================\n\n/**\n * Configuration options for the OHLCV service.\n */\nexport type OHLCVServiceOptions = {\n /** Optional callback to trace performance of OHLCV operations (default: no-op) */\n traceFn?: TraceCallback;\n};\n\n// =============================================================================\n// Action and Event Types\n// =============================================================================\n\nexport type OHLCVServiceActions = OHLCVServiceMethodActions;\n\nexport const OHLCV_SERVICE_ALLOWED_ACTIONS = [\n 'BackendWebSocketService:connect',\n 'BackendWebSocketService:forceReconnection',\n 'BackendWebSocketService:subscribe',\n 'BackendWebSocketService:getConnectionInfo',\n 'BackendWebSocketService:channelHasSubscription',\n 'BackendWebSocketService:getSubscriptionsByChannel',\n 'BackendWebSocketService:findSubscriptionsByChannelPrefix',\n 'BackendWebSocketService:addChannelCallback',\n 'BackendWebSocketService:removeChannelCallback',\n] as const;\n\nexport const OHLCV_SERVICE_ALLOWED_EVENTS = [\n 'BackendWebSocketService:connectionStateChanged',\n] as const;\n\nexport type AllowedActions = BackendWebSocketServiceMethodActions;\n\n// Events published by OHLCVService\n\nexport type OHLCVServiceBarUpdatedEvent = {\n type: `OHLCVService:barUpdated`;\n payload: [{ channel: string; bar: OHLCVBar }];\n};\n\nexport type OHLCVServiceChainStatusChangedEvent = {\n type: `OHLCVService:chainStatusChanged`;\n payload: [{ chainIds: string[]; status: 'up' | 'down'; timestamp?: number }];\n};\n\nexport type OHLCVServiceSubscriptionErrorEvent = {\n type: `OHLCVService:subscriptionError`;\n payload: [{ channel: string; error: string; operation: string }];\n};\n\nexport type OHLCVServiceEvents =\n | OHLCVServiceBarUpdatedEvent\n | OHLCVServiceChainStatusChangedEvent\n | OHLCVServiceSubscriptionErrorEvent;\n\nexport type AllowedEvents = BackendWebSocketServiceConnectionStateChangedEvent;\n\nexport type OHLCVServiceMessenger = Messenger<\n typeof SERVICE_NAME,\n OHLCVServiceActions | AllowedActions,\n OHLCVServiceEvents | AllowedEvents\n>;\n\n// =============================================================================\n// Main Service Class\n// =============================================================================\n\n/**\n * Service for real-time OHLCV candlestick streaming via the backend WebSocket\n * gateway. Communicates with {@link BackendWebSocketService} exclusively\n * through the messenger — no direct import of the class.\n *\n * Features:\n * - Reference counting: multiple UI consumers share one WebSocket subscription\n * - Grace-period unsubscribe: avoids rapid unsub/resub during navigation\n * - Idempotency: duplicate subscribe calls for the same channel are no-ops\n * - Reconnect resilience: resubscribes all active channels on reconnect\n * - Chain-status forwarding: listens to system-notifications for chain up/down\n *\n */\nexport class OHLCVService {\n readonly name = SERVICE_NAME;\n\n readonly #messenger: OHLCVServiceMessenger;\n\n readonly #trace: TraceCallback;\n\n readonly #channels = new Map<string, ChannelEntry>();\n\n readonly #channelLocks = new Map<string, Promise<void>>();\n\n readonly #chainsUp = new Set<string>();\n\n // =============================================================================\n // Constructor\n // =============================================================================\n\n constructor(\n options: OHLCVServiceOptions & { messenger: OHLCVServiceMessenger },\n ) {\n this.#messenger = options.messenger;\n\n this.#trace =\n options.traceFn ??\n ((<Result>(\n _request: TraceRequest,\n fn?: (context?: TraceContext) => Result,\n ) => fn?.()) as TraceCallback);\n\n this.#messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n\n this.#messenger.subscribe(\n 'BackendWebSocketService:connectionStateChanged',\n // eslint-disable-next-line @typescript-eslint/no-misused-promises\n (connectionInfo: WebSocketConnectionInfo) =>\n this.#handleWebSocketStateChange(connectionInfo),\n );\n }\n\n /**\n * Register the system-notifications channel callback.\n */\n init(): void {\n log('OHLCV-WS: Initializing — registering system-notifications callback');\n this.#messenger.call('BackendWebSocketService:addChannelCallback', {\n channelName: SYSTEM_NOTIFICATIONS_CHANNEL,\n callback: (notification: ServerNotificationMessage) =>\n this.#handleSystemNotification(notification),\n });\n }\n\n // =============================================================================\n // Public — Subscribe / Unsubscribe\n // =============================================================================\n\n /**\n * Subscribe to an OHLCV channel. If this is the first subscriber for the\n * given asset/interval/currency combination a WebSocket subscription is\n * created. Additional calls for the same combination only bump the reference\n * count.\n *\n * @param options - The subscription parameters.\n * @returns A promise that resolves once the subscription is established.\n */\n async subscribe(options: OHLCVSubscriptionOptions): Promise<void> {\n const channel = this.#buildChannel(options);\n return this.#withChannelLock(channel, () => this.#subscribeInner(channel));\n }\n\n async #subscribeInner(channel: string): Promise<void> {\n const entry = this.#channels.get(channel);\n\n if (entry?.gracePeriodTimer) {\n clearTimeout(entry.gracePeriodTimer);\n entry.gracePeriodTimer = undefined;\n entry.refCount += 1;\n log('OHLCV-WS: Cancelled grace-period unsubscribe, bumped refCount', {\n channel,\n refCount: entry.refCount,\n });\n return;\n }\n\n if (entry && entry.refCount > 0) {\n entry.refCount += 1;\n return;\n }\n\n // Flush other channels sitting in grace period to free server-side slots\n // and prevent accumulation when switching time ranges rapidly.\n for (const [otherChannel, otherEntry] of this.#channels.entries()) {\n if (otherChannel !== channel && otherEntry.gracePeriodTimer) {\n clearTimeout(otherEntry.gracePeriodTimer);\n otherEntry.gracePeriodTimer = undefined;\n log('OHLCV-WS: Flushing grace-period channel before new subscribe', {\n flushedChannel: otherChannel,\n newChannel: channel,\n });\n await this.#performUnsubscribe(otherChannel);\n }\n }\n\n try {\n await this.#messenger.call('BackendWebSocketService:connect');\n\n if (\n this.#messenger.call(\n 'BackendWebSocketService:channelHasSubscription',\n channel,\n )\n ) {\n log(\n 'OHLCV-WS: Channel already has WS subscription (idempotency), skipping',\n {\n channel,\n },\n );\n this.#channels.set(channel, { refCount: 1 });\n return;\n }\n\n await this.#messenger.call('BackendWebSocketService:subscribe', {\n channels: [channel],\n channelType: SUBSCRIPTION_NAMESPACE,\n callback: (notification: ServerNotificationMessage) => {\n this.#handleBarUpdate(channel, notification);\n },\n });\n\n this.#channels.set(channel, { refCount: 1 });\n log('OHLCV-WS: Subscribe succeeded — new WS subscription created', {\n channel,\n });\n } catch (error) {\n log('OHLCV-WS: Subscription failed, forcing reconnection', {\n channel,\n error,\n });\n this.#messenger.publish('OHLCVService:subscriptionError', {\n channel,\n error: String(error),\n operation: 'subscribe',\n });\n await this.#forceReconnection();\n }\n }\n\n /**\n * Unsubscribe from an OHLCV channel. Decrements the reference count and,\n * when it reaches zero, starts a grace-period timer before actually\n * unsubscribing from the WebSocket to absorb rapid navigation patterns.\n *\n * @param options - The subscription parameters to unsubscribe from.\n * @returns A promise that resolves once the unsubscription is processed.\n */\n async unsubscribe(options: OHLCVSubscriptionOptions): Promise<void> {\n const channel = this.#buildChannel(options);\n return this.#withChannelLock(channel, () =>\n this.#unsubscribeInner(channel),\n );\n }\n\n async #unsubscribeInner(channel: string): Promise<void> {\n const entry = this.#channels.get(channel);\n\n if (!entry || entry.refCount <= 0) {\n return;\n }\n\n entry.refCount -= 1;\n\n if (entry.refCount > 0) {\n return;\n }\n\n entry.gracePeriodTimer = setTimeout(() => {\n this.#performUnsubscribe(channel).catch(() => {\n // no-op\n });\n }, GRACE_PERIOD_MS);\n }\n\n // =============================================================================\n // Private — WebSocket Subscription Helpers\n // =============================================================================\n\n async #performUnsubscribe(channel: string): Promise<void> {\n log('OHLCV-WS: Grace period expired — performing actual WS unsubscribe', {\n channel,\n });\n this.#channels.delete(channel);\n\n try {\n const subscriptions = this.#messenger.call(\n 'BackendWebSocketService:getSubscriptionsByChannel',\n channel,\n );\n\n for (const sub of subscriptions) {\n await sub.unsubscribe();\n }\n log('OHLCV-WS: WS unsubscribe completed', { channel });\n } catch (error) {\n log('OHLCV-WS: Unsubscription failed, forcing reconnection', {\n channel,\n error,\n });\n this.#messenger.publish('OHLCVService:subscriptionError', {\n channel,\n error: String(error),\n operation: 'unsubscribe',\n });\n await this.#forceReconnection().catch(() => {\n // no-op\n });\n }\n }\n\n /**\n * Resubscribe all channels that were active before a disconnect.\n * Called when WebSocket transitions to CONNECTED.\n */\n async #resubscribeActiveChannels(): Promise<void> {\n const channelCount = this.#channels.size;\n log('OHLCV-WS: Resubscribing active channels after reconnect', {\n count: channelCount,\n });\n\n for (const [channel, entry] of this.#channels.entries()) {\n if (entry.refCount === 0) {\n continue;\n }\n\n try {\n if (\n this.#messenger.call(\n 'BackendWebSocketService:channelHasSubscription',\n channel,\n )\n ) {\n log(\n 'OHLCV-WS: Channel already subscribed on server, skipping resubscribe',\n {\n channel,\n },\n );\n continue;\n }\n\n await this.#messenger.call('BackendWebSocketService:subscribe', {\n channels: [channel],\n channelType: SUBSCRIPTION_NAMESPACE,\n callback: (notification: ServerNotificationMessage) => {\n this.#handleBarUpdate(channel, notification);\n },\n });\n log('OHLCV-WS: Resubscription succeeded', { channel });\n } catch (error) {\n log('OHLCV-WS: Resubscription failed for channel', { channel, error });\n }\n }\n }\n\n // =============================================================================\n // Private — Message Handlers\n // =============================================================================\n\n #handleBarUpdate(\n channel: string,\n notification: ServerNotificationMessage,\n ): void {\n const bar = notification.data as OHLCVBar;\n\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.#trace(\n {\n name: `${SERVICE_NAME} Bar Update`,\n data: { channel, timestamp: bar.timestamp },\n tags: { service: SERVICE_NAME },\n },\n () => {\n this.#messenger.publish('OHLCVService:barUpdated', { channel, bar });\n },\n );\n }\n\n #handleSystemNotification(notification: ServerNotificationMessage): void {\n const data = notification.data as OHLCVSystemNotificationData;\n const { timestamp } = notification;\n\n if (!data.chainIds || !Array.isArray(data.chainIds) || !data.status) {\n throw new Error(\n 'Invalid system notification data: missing chainIds or status',\n );\n }\n\n if (data.status === 'up') {\n for (const chainId of data.chainIds) {\n this.#chainsUp.add(chainId);\n }\n } else {\n for (const chainId of data.chainIds) {\n this.#chainsUp.delete(chainId);\n }\n }\n\n this.#messenger.publish('OHLCVService:chainStatusChanged', {\n chainIds: data.chainIds,\n status: data.status,\n timestamp,\n });\n\n log(`OHLCV-WS: Chain status change: ${data.status}`, {\n chains: data.chainIds,\n status: data.status,\n });\n }\n\n async #handleWebSocketStateChange(\n connectionInfo: WebSocketConnectionInfo,\n ): Promise<void> {\n const { state } = connectionInfo;\n\n if (state === WebSocketState.CONNECTED) {\n await this.#resubscribeActiveChannels();\n } else if (state === WebSocketState.DISCONNECTED) {\n const chainsToMarkDown = Array.from(this.#chainsUp);\n\n if (chainsToMarkDown.length > 0) {\n this.#messenger.publish('OHLCVService:chainStatusChanged', {\n chainIds: chainsToMarkDown,\n status: 'down',\n timestamp: Date.now(),\n });\n\n log(\n 'OHLCV-WS: WebSocket disconnection — marked tracked chains as down',\n {\n count: chainsToMarkDown.length,\n chains: chainsToMarkDown,\n },\n );\n\n this.#chainsUp.clear();\n }\n }\n }\n\n // =============================================================================\n // Private — Utility\n // =============================================================================\n\n #buildChannel(options: OHLCVSubscriptionOptions): string {\n return `${SUBSCRIPTION_NAMESPACE}.${options.assetId}.${options.interval}.${options.currency}`;\n }\n\n /**\n * Serialize async operations for the same channel so that concurrent\n * subscribe/unsubscribe calls cannot interleave and corrupt refCount.\n * Different channels are not blocked by each other.\n *\n * @param channel - The channel key to lock on.\n * @param fn - The async function to execute under the lock.\n */\n async #withChannelLock(\n channel: string,\n fn: () => Promise<void>,\n ): Promise<void> {\n const prev = this.#channelLocks.get(channel) ?? Promise.resolve();\n const next = prev.then(fn, fn);\n this.#channelLocks.set(channel, next);\n try {\n await next;\n } finally {\n if (this.#channelLocks.get(channel) === next) {\n this.#channelLocks.delete(channel);\n }\n }\n }\n\n async #forceReconnection(): Promise<void> {\n log('OHLCV-WS: Forcing WebSocket reconnection');\n await this.#messenger.call('BackendWebSocketService:forceReconnection');\n }\n\n // =============================================================================\n // Public — Cleanup\n // =============================================================================\n\n /**\n * Destroy the service and clean up all resources.\n */\n destroy(): void {\n for (const entry of this.#channels.values()) {\n if (entry.gracePeriodTimer) {\n clearTimeout(entry.gracePeriodTimer);\n }\n }\n this.#channels.clear();\n this.#channelLocks.clear();\n\n this.#messenger.call(\n 'BackendWebSocketService:removeChannelCallback',\n SYSTEM_NOTIFICATIONS_CHANNEL,\n );\n }\n}\n"]}
1
+ {"version":3,"file":"OHLCVService.cjs","sourceRoot":"","sources":["../../../src/ws/ohlcv/OHLCVService.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OHLCV Service for real-time candlestick data streaming via WebSocket.\n *\n * Wraps {@link BackendWebSocketService} through the messenger pattern to\n * provide subscribe/unsubscribe semantics for OHLCV market-data channels.\n * Includes reference counting, grace-period unsubscribe, idempotency checks,\n * chain-status forwarding, and automatic resubscription on reconnect.\n */\n\nimport type {\n TraceCallback,\n TraceContext,\n TraceRequest,\n} from '@metamask/controller-utils';\nimport type { Messenger } from '@metamask/messenger';\nimport { Mutex } from 'async-mutex';\n\nimport { projectLogger, createModuleLogger } from '../../logger';\nimport type {\n WebSocketConnectionInfo,\n BackendWebSocketServiceConnectionStateChangedEvent,\n ServerNotificationMessage,\n} from '../BackendWebSocketService';\nimport { WebSocketState } from '../BackendWebSocketService';\nimport type { BackendWebSocketServiceMethodActions } from '../BackendWebSocketService-method-action-types';\nimport type { OHLCVServiceMethodActions } from './OHLCVService-method-action-types';\nimport type { OHLCVBar, OHLCVSubscriptionOptions } from './types';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst SERVICE_NAME = 'OHLCVService';\n\nconst log = createModuleLogger(projectLogger, SERVICE_NAME);\n\nconst MESSENGER_EXPOSED_METHODS = ['subscribe', 'unsubscribe'] as const;\n\nconst SUBSCRIPTION_NAMESPACE = 'market-data.v1';\n\nconst SYSTEM_NOTIFICATIONS_CHANNEL = `system-notifications.v1.${SUBSCRIPTION_NAMESPACE}`;\n\n/** Delay before actually unsubscribing from a channel after refCount reaches 0. */\nconst GRACE_PERIOD_MS = 3_000;\n\n// =============================================================================\n// Types — Channel Tracking\n// =============================================================================\n\ntype ChannelEntry = {\n refCount: number;\n gracePeriodTimer?: ReturnType<typeof setTimeout>;\n};\n\n// =============================================================================\n// Types — System Notifications\n// =============================================================================\n\n/**\n * System notification data for chain status updates on market-data channels.\n */\nexport type OHLCVSystemNotificationData = {\n chainIds: string[];\n status: 'down' | 'up';\n timestamp?: number;\n};\n\n// =============================================================================\n// Types — Service Options\n// =============================================================================\n\n/**\n * Configuration options for the OHLCV service.\n */\nexport type OHLCVServiceOptions = {\n /** Optional callback to trace performance of OHLCV operations (default: no-op) */\n traceFn?: TraceCallback;\n};\n\n// =============================================================================\n// Action and Event Types\n// =============================================================================\n\nexport type OHLCVServiceActions = OHLCVServiceMethodActions;\n\nexport const OHLCV_SERVICE_ALLOWED_ACTIONS = [\n 'BackendWebSocketService:connect',\n 'BackendWebSocketService:forceReconnection',\n 'BackendWebSocketService:subscribe',\n 'BackendWebSocketService:getConnectionInfo',\n 'BackendWebSocketService:channelHasSubscription',\n 'BackendWebSocketService:getSubscriptionsByChannel',\n 'BackendWebSocketService:findSubscriptionsByChannelPrefix',\n 'BackendWebSocketService:addChannelCallback',\n 'BackendWebSocketService:removeChannelCallback',\n] as const;\n\nexport const OHLCV_SERVICE_ALLOWED_EVENTS = [\n 'BackendWebSocketService:connectionStateChanged',\n] as const;\n\nexport type AllowedActions = BackendWebSocketServiceMethodActions;\n\n// Events published by OHLCVService\n\nexport type OHLCVServiceBarUpdatedEvent = {\n type: `OHLCVService:barUpdated`;\n payload: [{ channel: string; bar: OHLCVBar }];\n};\n\nexport type OHLCVServiceChainStatusChangedEvent = {\n type: `OHLCVService:chainStatusChanged`;\n payload: [{ chainIds: string[]; status: 'up' | 'down'; timestamp?: number }];\n};\n\nexport type OHLCVServiceSubscriptionErrorEvent = {\n type: `OHLCVService:subscriptionError`;\n payload: [{ channel: string; error: string; operation: string }];\n};\n\nexport type OHLCVServiceEvents =\n | OHLCVServiceBarUpdatedEvent\n | OHLCVServiceChainStatusChangedEvent\n | OHLCVServiceSubscriptionErrorEvent;\n\nexport type AllowedEvents = BackendWebSocketServiceConnectionStateChangedEvent;\n\nexport type OHLCVServiceMessenger = Messenger<\n typeof SERVICE_NAME,\n OHLCVServiceActions | AllowedActions,\n OHLCVServiceEvents | AllowedEvents\n>;\n\n// =============================================================================\n// Main Service Class\n// =============================================================================\n\n/**\n * Service for real-time OHLCV candlestick streaming via the backend WebSocket\n * gateway. Communicates with {@link BackendWebSocketService} exclusively\n * through the messenger — no direct import of the class.\n *\n * Features:\n * - Reference counting: multiple UI consumers share one WebSocket subscription\n * - Grace-period unsubscribe: avoids rapid unsub/resub during navigation\n * - Idempotency: duplicate subscribe calls for the same channel are no-ops\n * - Reconnect resilience: resubscribes all active channels on reconnect\n * - Chain-status forwarding: listens to system-notifications for chain up/down\n *\n */\nexport class OHLCVService {\n readonly name = SERVICE_NAME;\n\n readonly #messenger: OHLCVServiceMessenger;\n\n readonly #trace: TraceCallback;\n\n readonly #channels = new Map<string, ChannelEntry>();\n\n readonly #mutex = new Mutex();\n\n readonly #chainsUp = new Set<string>();\n\n // =============================================================================\n // Constructor\n // =============================================================================\n\n constructor(\n options: OHLCVServiceOptions & { messenger: OHLCVServiceMessenger },\n ) {\n this.#messenger = options.messenger;\n\n this.#trace =\n options.traceFn ??\n ((<Result>(\n _request: TraceRequest,\n fn?: (context?: TraceContext) => Result,\n ) => fn?.()) as TraceCallback);\n\n this.#messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n\n this.#messenger.subscribe(\n 'BackendWebSocketService:connectionStateChanged',\n // eslint-disable-next-line @typescript-eslint/no-misused-promises\n (connectionInfo: WebSocketConnectionInfo) =>\n this.#handleWebSocketStateChange(connectionInfo),\n );\n }\n\n /**\n * Register the system-notifications channel callback.\n */\n init(): void {\n log('OHLCV-WS: Initializing — registering system-notifications callback');\n this.#messenger.call('BackendWebSocketService:addChannelCallback', {\n channelName: SYSTEM_NOTIFICATIONS_CHANNEL,\n callback: (notification: ServerNotificationMessage) =>\n this.#handleSystemNotification(notification),\n });\n }\n\n // =============================================================================\n // Public — Subscribe / Unsubscribe\n // =============================================================================\n\n /**\n * Subscribe to an OHLCV channel. If this is the first subscriber for the\n * given asset/interval/currency combination a WebSocket subscription is\n * created. Additional calls for the same combination only bump the reference\n * count.\n *\n * @param options - The subscription parameters.\n * @returns A promise that resolves once the subscription is established.\n */\n async subscribe(options: OHLCVSubscriptionOptions): Promise<void> {\n const channel = this.#buildChannel(options);\n const releaseLock = await this.#mutex.acquire();\n try {\n await this.#subscribeInner(channel);\n } finally {\n releaseLock();\n }\n }\n\n async #subscribeInner(channel: string): Promise<void> {\n const entry = this.#channels.get(channel);\n\n if (entry?.gracePeriodTimer) {\n clearTimeout(entry.gracePeriodTimer);\n entry.gracePeriodTimer = undefined;\n entry.refCount += 1;\n log('OHLCV-WS: Cancelled grace-period unsubscribe, bumped refCount', {\n channel,\n refCount: entry.refCount,\n });\n\n if (\n this.#messenger.call(\n 'BackendWebSocketService:channelHasSubscription',\n channel,\n )\n ) {\n return;\n }\n // WS subscription was lost (e.g. after disconnect/reconnect) — fall\n // through to recreate it while keeping the already-bumped refCount.\n } else if (entry && entry.refCount > 0) {\n entry.refCount += 1;\n return;\n }\n\n try {\n await this.#messenger.call('BackendWebSocketService:connect');\n\n if (\n this.#messenger.call(\n 'BackendWebSocketService:channelHasSubscription',\n channel,\n )\n ) {\n log(\n 'OHLCV-WS: Channel already has WS subscription (idempotency), skipping',\n {\n channel,\n },\n );\n this.#channels.set(channel, { refCount: 1 });\n return;\n }\n\n await this.#messenger.call('BackendWebSocketService:subscribe', {\n channels: [channel],\n channelType: SUBSCRIPTION_NAMESPACE,\n callback: (notification: ServerNotificationMessage) => {\n this.#handleBarUpdate(channel, notification);\n },\n });\n\n this.#channels.set(channel, { refCount: 1 });\n log('OHLCV-WS: Subscribe succeeded — new WS subscription created', {\n channel,\n });\n } catch (error) {\n log('OHLCV-WS: Subscription failed, forcing reconnection', {\n channel,\n error,\n });\n this.#messenger.publish('OHLCVService:subscriptionError', {\n channel,\n error: String(error),\n operation: 'subscribe',\n });\n await this.#forceReconnection();\n }\n }\n\n /**\n * Unsubscribe from an OHLCV channel. Decrements the reference count and,\n * when it reaches zero, starts a grace-period timer before actually\n * unsubscribing from the WebSocket to absorb rapid navigation patterns.\n *\n * @param options - The subscription parameters to unsubscribe from.\n * @returns A promise that resolves once the unsubscription is processed.\n */\n async unsubscribe(options: OHLCVSubscriptionOptions): Promise<void> {\n const channel = this.#buildChannel(options);\n const releaseLock = await this.#mutex.acquire();\n try {\n await this.#unsubscribeInner(channel);\n } finally {\n releaseLock();\n }\n }\n\n async #unsubscribeInner(channel: string): Promise<void> {\n const entry = this.#channels.get(channel);\n\n if (!entry || entry.refCount <= 0) {\n return;\n }\n\n entry.refCount -= 1;\n\n if (entry.refCount > 0) {\n return;\n }\n\n entry.gracePeriodTimer = setTimeout(() => {\n entry.gracePeriodTimer = undefined;\n this.#performUnsubscribe(channel).catch(() => {\n // no-op\n });\n }, GRACE_PERIOD_MS);\n }\n\n // =============================================================================\n // Private — WebSocket Subscription Helpers\n // =============================================================================\n\n async #performUnsubscribe(channel: string): Promise<void> {\n const releaseLock = await this.#mutex.acquire();\n try {\n const entry = this.#channels.get(channel);\n if (entry && entry.refCount > 0) {\n log(\n 'OHLCV-WS: Skipping unsubscribe — new subscriber arrived while queued',\n { channel, refCount: entry.refCount },\n );\n return;\n }\n\n log('OHLCV-WS: Grace period expired — performing actual WS unsubscribe', {\n channel,\n });\n this.#channels.delete(channel);\n\n try {\n const subscriptions = this.#messenger.call(\n 'BackendWebSocketService:getSubscriptionsByChannel',\n channel,\n );\n\n for (const sub of subscriptions) {\n await sub.unsubscribe();\n }\n log('OHLCV-WS: WS unsubscribe completed', { channel });\n } catch (error) {\n log('OHLCV-WS: Unsubscription failed, forcing reconnection', {\n channel,\n error,\n });\n this.#messenger.publish('OHLCVService:subscriptionError', {\n channel,\n error: String(error),\n operation: 'unsubscribe',\n });\n await this.#forceReconnection().catch(() => {\n // no-op\n });\n }\n } finally {\n releaseLock();\n }\n }\n\n /**\n * Resubscribe all channels that were active before a disconnect.\n * Called when WebSocket transitions to CONNECTED.\n */\n async #resubscribeActiveChannels(): Promise<void> {\n const releaseLock = await this.#mutex.acquire();\n try {\n const channelCount = this.#channels.size;\n log('OHLCV-WS: Resubscribing active channels after reconnect', {\n count: channelCount,\n });\n\n for (const [channel, entry] of this.#channels.entries()) {\n if (entry.refCount === 0) {\n continue;\n }\n\n try {\n if (\n this.#messenger.call(\n 'BackendWebSocketService:channelHasSubscription',\n channel,\n )\n ) {\n log(\n 'OHLCV-WS: Channel already subscribed on server, skipping resubscribe',\n {\n channel,\n },\n );\n continue;\n }\n\n await this.#messenger.call('BackendWebSocketService:subscribe', {\n channels: [channel],\n channelType: SUBSCRIPTION_NAMESPACE,\n callback: (notification: ServerNotificationMessage) => {\n this.#handleBarUpdate(channel, notification);\n },\n });\n log('OHLCV-WS: Resubscription succeeded', { channel });\n } catch (error) {\n log('OHLCV-WS: Resubscription failed for channel', {\n channel,\n error,\n });\n }\n }\n } finally {\n releaseLock();\n }\n }\n\n // =============================================================================\n // Private — Message Handlers\n // =============================================================================\n\n #handleBarUpdate(\n channel: string,\n notification: ServerNotificationMessage,\n ): void {\n const bar = notification.data as OHLCVBar;\n\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.#trace(\n {\n name: `${SERVICE_NAME} Bar Update`,\n data: { channel, timestamp: bar.timestamp },\n tags: { service: SERVICE_NAME },\n },\n () => {\n this.#messenger.publish('OHLCVService:barUpdated', { channel, bar });\n },\n );\n }\n\n #handleSystemNotification(notification: ServerNotificationMessage): void {\n const data = notification.data as OHLCVSystemNotificationData;\n const { timestamp } = notification;\n\n if (!data.chainIds || !Array.isArray(data.chainIds) || !data.status) {\n throw new Error(\n 'Invalid system notification data: missing chainIds or status',\n );\n }\n\n if (data.status === 'up') {\n for (const chainId of data.chainIds) {\n this.#chainsUp.add(chainId);\n }\n } else {\n for (const chainId of data.chainIds) {\n this.#chainsUp.delete(chainId);\n }\n }\n\n this.#messenger.publish('OHLCVService:chainStatusChanged', {\n chainIds: data.chainIds,\n status: data.status,\n timestamp,\n });\n\n log(`OHLCV-WS: Chain status change: ${data.status}`, {\n chains: data.chainIds,\n status: data.status,\n });\n }\n\n async #handleWebSocketStateChange(\n connectionInfo: WebSocketConnectionInfo,\n ): Promise<void> {\n const { state } = connectionInfo;\n\n if (state === WebSocketState.CONNECTED) {\n await this.#resubscribeActiveChannels();\n } else if (state === WebSocketState.DISCONNECTED) {\n const chainsToMarkDown = Array.from(this.#chainsUp);\n\n if (chainsToMarkDown.length > 0) {\n this.#messenger.publish('OHLCVService:chainStatusChanged', {\n chainIds: chainsToMarkDown,\n status: 'down',\n timestamp: Date.now(),\n });\n\n log(\n 'OHLCV-WS: WebSocket disconnection — marked tracked chains as down',\n {\n count: chainsToMarkDown.length,\n chains: chainsToMarkDown,\n },\n );\n\n this.#chainsUp.clear();\n }\n }\n }\n\n // =============================================================================\n // Private — Utility\n // =============================================================================\n\n #buildChannel(options: OHLCVSubscriptionOptions): string {\n return `${SUBSCRIPTION_NAMESPACE}.${options.assetId}.${options.interval}.${options.currency}`;\n }\n\n async #forceReconnection(): Promise<void> {\n log('OHLCV-WS: Forcing WebSocket reconnection');\n await this.#messenger.call('BackendWebSocketService:forceReconnection');\n }\n\n // =============================================================================\n // Public — Cleanup\n // =============================================================================\n\n /**\n * Destroy the service and clean up all resources.\n */\n destroy(): void {\n for (const entry of this.#channels.values()) {\n if (entry.gracePeriodTimer) {\n clearTimeout(entry.gracePeriodTimer);\n }\n }\n this.#channels.clear();\n this.#chainsUp.clear();\n\n this.#messenger.call(\n 'BackendWebSocketService:removeChannelCallback',\n SYSTEM_NOTIFICATIONS_CHANNEL,\n );\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"OHLCVService.d.cts","sourceRoot":"","sources":["../../../src/ws/ohlcv/OHLCVService.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,aAAa,EAGd,mCAAmC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAGrD,OAAO,KAAK,EAEV,kDAAkD,EAEnD,uCAAmC;AAEpC,OAAO,KAAK,EAAE,oCAAoC,EAAE,2DAAuD;AAC3G,OAAO,KAAK,EAAE,yBAAyB,EAAE,+CAA2C;AACpF,OAAO,KAAK,EAAE,QAAQ,EAAE,wBAAwB,EAAE,oBAAgB;AAMlE,QAAA,MAAM,YAAY,iBAAiB,CAAC;AA0BpC;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAMF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,kFAAkF;IAClF,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB,CAAC;AAMF,MAAM,MAAM,mBAAmB,GAAG,yBAAyB,CAAC;AAE5D,eAAO,MAAM,6BAA6B,+aAUhC,CAAC;AAEX,eAAO,MAAM,4BAA4B,6DAE/B,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,oCAAoC,CAAC;AAIlE,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,mCAAmC,GAAG;IAChD,IAAI,EAAE,iCAAiC,CAAC;IACxC,OAAO,EAAE,CAAC;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,IAAI,GAAG,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9E,CAAC;AAEF,MAAM,MAAM,kCAAkC,GAAG;IAC/C,IAAI,EAAE,gCAAgC,CAAC;IACvC,OAAO,EAAE,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClE,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B,2BAA2B,GAC3B,mCAAmC,GACnC,kCAAkC,CAAC;AAEvC,MAAM,MAAM,aAAa,GAAG,kDAAkD,CAAC;AAE/E,MAAM,MAAM,qBAAqB,GAAG,SAAS,CAC3C,OAAO,YAAY,EACnB,mBAAmB,GAAG,cAAc,EACpC,kBAAkB,GAAG,aAAa,CACnC,CAAC;AAMF;;;;;;;;;;;;GAYG;AACH,qBAAa,YAAY;;IACvB,QAAQ,CAAC,IAAI,kBAAgB;gBAiB3B,OAAO,EAAE,mBAAmB,GAAG;QAAE,SAAS,EAAE,qBAAqB,CAAA;KAAE;IAwBrE;;OAEG;IACH,IAAI,IAAI,IAAI;IAaZ;;;;;;;;OAQG;IACG,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmFjE;;;;;;;OAOG;IACG,WAAW,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IA0OnE;;OAEG;IACH,OAAO,IAAI,IAAI;CAchB"}
1
+ {"version":3,"file":"OHLCVService.d.cts","sourceRoot":"","sources":["../../../src/ws/ohlcv/OHLCVService.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,aAAa,EAGd,mCAAmC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAIrD,OAAO,KAAK,EAEV,kDAAkD,EAEnD,uCAAmC;AAEpC,OAAO,KAAK,EAAE,oCAAoC,EAAE,2DAAuD;AAC3G,OAAO,KAAK,EAAE,yBAAyB,EAAE,+CAA2C;AACpF,OAAO,KAAK,EAAE,QAAQ,EAAE,wBAAwB,EAAE,oBAAgB;AAMlE,QAAA,MAAM,YAAY,iBAAiB,CAAC;AA0BpC;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAMF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,kFAAkF;IAClF,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB,CAAC;AAMF,MAAM,MAAM,mBAAmB,GAAG,yBAAyB,CAAC;AAE5D,eAAO,MAAM,6BAA6B,+aAUhC,CAAC;AAEX,eAAO,MAAM,4BAA4B,6DAE/B,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,oCAAoC,CAAC;AAIlE,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,mCAAmC,GAAG;IAChD,IAAI,EAAE,iCAAiC,CAAC;IACxC,OAAO,EAAE,CAAC;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,IAAI,GAAG,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9E,CAAC;AAEF,MAAM,MAAM,kCAAkC,GAAG;IAC/C,IAAI,EAAE,gCAAgC,CAAC;IACvC,OAAO,EAAE,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClE,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B,2BAA2B,GAC3B,mCAAmC,GACnC,kCAAkC,CAAC;AAEvC,MAAM,MAAM,aAAa,GAAG,kDAAkD,CAAC;AAE/E,MAAM,MAAM,qBAAqB,GAAG,SAAS,CAC3C,OAAO,YAAY,EACnB,mBAAmB,GAAG,cAAc,EACpC,kBAAkB,GAAG,aAAa,CACnC,CAAC;AAMF;;;;;;;;;;;;GAYG;AACH,qBAAa,YAAY;;IACvB,QAAQ,CAAC,IAAI,kBAAgB;gBAiB3B,OAAO,EAAE,mBAAmB,GAAG;QAAE,SAAS,EAAE,qBAAqB,CAAA;KAAE;IAwBrE;;OAEG;IACH,IAAI,IAAI,IAAI;IAaZ;;;;;;;;OAQG;IACG,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkFjE;;;;;;;OAOG;IACG,WAAW,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IA4OnE;;OAEG;IACH,OAAO,IAAI,IAAI;CAchB"}
@@ -1 +1 @@
1
- {"version":3,"file":"OHLCVService.d.mts","sourceRoot":"","sources":["../../../src/ws/ohlcv/OHLCVService.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,aAAa,EAGd,mCAAmC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAGrD,OAAO,KAAK,EAEV,kDAAkD,EAEnD,uCAAmC;AAEpC,OAAO,KAAK,EAAE,oCAAoC,EAAE,2DAAuD;AAC3G,OAAO,KAAK,EAAE,yBAAyB,EAAE,+CAA2C;AACpF,OAAO,KAAK,EAAE,QAAQ,EAAE,wBAAwB,EAAE,oBAAgB;AAMlE,QAAA,MAAM,YAAY,iBAAiB,CAAC;AA0BpC;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAMF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,kFAAkF;IAClF,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB,CAAC;AAMF,MAAM,MAAM,mBAAmB,GAAG,yBAAyB,CAAC;AAE5D,eAAO,MAAM,6BAA6B,+aAUhC,CAAC;AAEX,eAAO,MAAM,4BAA4B,6DAE/B,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,oCAAoC,CAAC;AAIlE,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,mCAAmC,GAAG;IAChD,IAAI,EAAE,iCAAiC,CAAC;IACxC,OAAO,EAAE,CAAC;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,IAAI,GAAG,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9E,CAAC;AAEF,MAAM,MAAM,kCAAkC,GAAG;IAC/C,IAAI,EAAE,gCAAgC,CAAC;IACvC,OAAO,EAAE,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClE,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B,2BAA2B,GAC3B,mCAAmC,GACnC,kCAAkC,CAAC;AAEvC,MAAM,MAAM,aAAa,GAAG,kDAAkD,CAAC;AAE/E,MAAM,MAAM,qBAAqB,GAAG,SAAS,CAC3C,OAAO,YAAY,EACnB,mBAAmB,GAAG,cAAc,EACpC,kBAAkB,GAAG,aAAa,CACnC,CAAC;AAMF;;;;;;;;;;;;GAYG;AACH,qBAAa,YAAY;;IACvB,QAAQ,CAAC,IAAI,kBAAgB;gBAiB3B,OAAO,EAAE,mBAAmB,GAAG;QAAE,SAAS,EAAE,qBAAqB,CAAA;KAAE;IAwBrE;;OAEG;IACH,IAAI,IAAI,IAAI;IAaZ;;;;;;;;OAQG;IACG,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmFjE;;;;;;;OAOG;IACG,WAAW,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IA0OnE;;OAEG;IACH,OAAO,IAAI,IAAI;CAchB"}
1
+ {"version":3,"file":"OHLCVService.d.mts","sourceRoot":"","sources":["../../../src/ws/ohlcv/OHLCVService.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,aAAa,EAGd,mCAAmC;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AAIrD,OAAO,KAAK,EAEV,kDAAkD,EAEnD,uCAAmC;AAEpC,OAAO,KAAK,EAAE,oCAAoC,EAAE,2DAAuD;AAC3G,OAAO,KAAK,EAAE,yBAAyB,EAAE,+CAA2C;AACpF,OAAO,KAAK,EAAE,QAAQ,EAAE,wBAAwB,EAAE,oBAAgB;AAMlE,QAAA,MAAM,YAAY,iBAAiB,CAAC;AA0BpC;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAMF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,kFAAkF;IAClF,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB,CAAC;AAMF,MAAM,MAAM,mBAAmB,GAAG,yBAAyB,CAAC;AAE5D,eAAO,MAAM,6BAA6B,+aAUhC,CAAC;AAEX,eAAO,MAAM,4BAA4B,6DAE/B,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,oCAAoC,CAAC;AAIlE,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,yBAAyB,CAAC;IAChC,OAAO,EAAE,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC;CAC/C,CAAC;AAEF,MAAM,MAAM,mCAAmC,GAAG;IAChD,IAAI,EAAE,iCAAiC,CAAC;IACxC,OAAO,EAAE,CAAC;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,IAAI,GAAG,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9E,CAAC;AAEF,MAAM,MAAM,kCAAkC,GAAG;IAC/C,IAAI,EAAE,gCAAgC,CAAC;IACvC,OAAO,EAAE,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClE,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B,2BAA2B,GAC3B,mCAAmC,GACnC,kCAAkC,CAAC;AAEvC,MAAM,MAAM,aAAa,GAAG,kDAAkD,CAAC;AAE/E,MAAM,MAAM,qBAAqB,GAAG,SAAS,CAC3C,OAAO,YAAY,EACnB,mBAAmB,GAAG,cAAc,EACpC,kBAAkB,GAAG,aAAa,CACnC,CAAC;AAMF;;;;;;;;;;;;GAYG;AACH,qBAAa,YAAY;;IACvB,QAAQ,CAAC,IAAI,kBAAgB;gBAiB3B,OAAO,EAAE,mBAAmB,GAAG;QAAE,SAAS,EAAE,qBAAqB,CAAA;KAAE;IAwBrE;;OAEG;IACH,IAAI,IAAI,IAAI;IAaZ;;;;;;;;OAQG;IACG,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkFjE;;;;;;;OAOG;IACG,WAAW,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IA4OnE;;OAEG;IACH,OAAO,IAAI,IAAI;CAchB"}
@@ -17,7 +17,8 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
17
17
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
18
18
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
19
19
  };
20
- var _OHLCVService_instances, _OHLCVService_messenger, _OHLCVService_trace, _OHLCVService_channels, _OHLCVService_channelLocks, _OHLCVService_chainsUp, _OHLCVService_subscribeInner, _OHLCVService_unsubscribeInner, _OHLCVService_performUnsubscribe, _OHLCVService_resubscribeActiveChannels, _OHLCVService_handleBarUpdate, _OHLCVService_handleSystemNotification, _OHLCVService_handleWebSocketStateChange, _OHLCVService_buildChannel, _OHLCVService_withChannelLock, _OHLCVService_forceReconnection;
20
+ var _OHLCVService_instances, _OHLCVService_messenger, _OHLCVService_trace, _OHLCVService_channels, _OHLCVService_mutex, _OHLCVService_chainsUp, _OHLCVService_subscribeInner, _OHLCVService_unsubscribeInner, _OHLCVService_performUnsubscribe, _OHLCVService_resubscribeActiveChannels, _OHLCVService_handleBarUpdate, _OHLCVService_handleSystemNotification, _OHLCVService_handleWebSocketStateChange, _OHLCVService_buildChannel, _OHLCVService_forceReconnection;
21
+ import { Mutex } from "async-mutex";
21
22
  import { projectLogger, createModuleLogger } from "../../logger.mjs";
22
23
  import { WebSocketState } from "../BackendWebSocketService.mjs";
23
24
  // =============================================================================
@@ -70,7 +71,7 @@ export class OHLCVService {
70
71
  _OHLCVService_messenger.set(this, void 0);
71
72
  _OHLCVService_trace.set(this, void 0);
72
73
  _OHLCVService_channels.set(this, new Map());
73
- _OHLCVService_channelLocks.set(this, new Map());
74
+ _OHLCVService_mutex.set(this, new Mutex());
74
75
  _OHLCVService_chainsUp.set(this, new Set());
75
76
  __classPrivateFieldSet(this, _OHLCVService_messenger, options.messenger, "f");
76
77
  __classPrivateFieldSet(this, _OHLCVService_trace, options.traceFn ??
@@ -104,7 +105,13 @@ export class OHLCVService {
104
105
  */
105
106
  async subscribe(options) {
106
107
  const channel = __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_buildChannel).call(this, options);
107
- return __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_withChannelLock).call(this, channel, () => __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_subscribeInner).call(this, channel));
108
+ const releaseLock = await __classPrivateFieldGet(this, _OHLCVService_mutex, "f").acquire();
109
+ try {
110
+ await __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_subscribeInner).call(this, channel);
111
+ }
112
+ finally {
113
+ releaseLock();
114
+ }
108
115
  }
109
116
  /**
110
117
  * Unsubscribe from an OHLCV channel. Decrements the reference count and,
@@ -116,7 +123,13 @@ export class OHLCVService {
116
123
  */
117
124
  async unsubscribe(options) {
118
125
  const channel = __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_buildChannel).call(this, options);
119
- return __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_withChannelLock).call(this, channel, () => __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_unsubscribeInner).call(this, channel));
126
+ const releaseLock = await __classPrivateFieldGet(this, _OHLCVService_mutex, "f").acquire();
127
+ try {
128
+ await __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_unsubscribeInner).call(this, channel);
129
+ }
130
+ finally {
131
+ releaseLock();
132
+ }
120
133
  }
121
134
  // =============================================================================
122
135
  // Public — Cleanup
@@ -131,11 +144,11 @@ export class OHLCVService {
131
144
  }
132
145
  }
133
146
  __classPrivateFieldGet(this, _OHLCVService_channels, "f").clear();
134
- __classPrivateFieldGet(this, _OHLCVService_channelLocks, "f").clear();
147
+ __classPrivateFieldGet(this, _OHLCVService_chainsUp, "f").clear();
135
148
  __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:removeChannelCallback', SYSTEM_NOTIFICATIONS_CHANNEL);
136
149
  }
137
150
  }
138
- _OHLCVService_messenger = new WeakMap(), _OHLCVService_trace = new WeakMap(), _OHLCVService_channels = new WeakMap(), _OHLCVService_channelLocks = new WeakMap(), _OHLCVService_chainsUp = new WeakMap(), _OHLCVService_instances = new WeakSet(), _OHLCVService_subscribeInner = async function _OHLCVService_subscribeInner(channel) {
151
+ _OHLCVService_messenger = new WeakMap(), _OHLCVService_trace = new WeakMap(), _OHLCVService_channels = new WeakMap(), _OHLCVService_mutex = new WeakMap(), _OHLCVService_chainsUp = new WeakMap(), _OHLCVService_instances = new WeakSet(), _OHLCVService_subscribeInner = async function _OHLCVService_subscribeInner(channel) {
139
152
  const entry = __classPrivateFieldGet(this, _OHLCVService_channels, "f").get(channel);
140
153
  if (entry?.gracePeriodTimer) {
141
154
  clearTimeout(entry.gracePeriodTimer);
@@ -145,25 +158,16 @@ _OHLCVService_messenger = new WeakMap(), _OHLCVService_trace = new WeakMap(), _O
145
158
  channel,
146
159
  refCount: entry.refCount,
147
160
  });
148
- return;
161
+ if (__classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:channelHasSubscription', channel)) {
162
+ return;
163
+ }
164
+ // WS subscription was lost (e.g. after disconnect/reconnect) — fall
165
+ // through to recreate it while keeping the already-bumped refCount.
149
166
  }
150
- if (entry && entry.refCount > 0) {
167
+ else if (entry && entry.refCount > 0) {
151
168
  entry.refCount += 1;
152
169
  return;
153
170
  }
154
- // Flush other channels sitting in grace period to free server-side slots
155
- // and prevent accumulation when switching time ranges rapidly.
156
- for (const [otherChannel, otherEntry] of __classPrivateFieldGet(this, _OHLCVService_channels, "f").entries()) {
157
- if (otherChannel !== channel && otherEntry.gracePeriodTimer) {
158
- clearTimeout(otherEntry.gracePeriodTimer);
159
- otherEntry.gracePeriodTimer = undefined;
160
- log('OHLCV-WS: Flushing grace-period channel before new subscribe', {
161
- flushedChannel: otherChannel,
162
- newChannel: channel,
163
- });
164
- await __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_performUnsubscribe).call(this, otherChannel);
165
- }
166
- }
167
171
  try {
168
172
  await __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:connect');
169
173
  if (__classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:channelHasSubscription', channel)) {
@@ -207,6 +211,7 @@ _OHLCVService_messenger = new WeakMap(), _OHLCVService_trace = new WeakMap(), _O
207
211
  return;
208
212
  }
209
213
  entry.gracePeriodTimer = setTimeout(() => {
214
+ entry.gracePeriodTimer = undefined;
210
215
  __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_performUnsubscribe).call(this, channel).catch(() => {
211
216
  // no-op
212
217
  });
@@ -216,30 +221,41 @@ _OHLCVService_messenger = new WeakMap(), _OHLCVService_trace = new WeakMap(), _O
216
221
  // Private — WebSocket Subscription Helpers
217
222
  // =============================================================================
218
223
  async function _OHLCVService_performUnsubscribe(channel) {
219
- log('OHLCV-WS: Grace period expired performing actual WS unsubscribe', {
220
- channel,
221
- });
222
- __classPrivateFieldGet(this, _OHLCVService_channels, "f").delete(channel);
224
+ const releaseLock = await __classPrivateFieldGet(this, _OHLCVService_mutex, "f").acquire();
223
225
  try {
224
- const subscriptions = __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:getSubscriptionsByChannel', channel);
225
- for (const sub of subscriptions) {
226
- await sub.unsubscribe();
226
+ const entry = __classPrivateFieldGet(this, _OHLCVService_channels, "f").get(channel);
227
+ if (entry && entry.refCount > 0) {
228
+ log('OHLCV-WS: Skipping unsubscribe — new subscriber arrived while queued', { channel, refCount: entry.refCount });
229
+ return;
227
230
  }
228
- log('OHLCV-WS: WS unsubscribe completed', { channel });
229
- }
230
- catch (error) {
231
- log('OHLCV-WS: Unsubscription failed, forcing reconnection', {
232
- channel,
233
- error,
234
- });
235
- __classPrivateFieldGet(this, _OHLCVService_messenger, "f").publish('OHLCVService:subscriptionError', {
231
+ log('OHLCV-WS: Grace period expired — performing actual WS unsubscribe', {
236
232
  channel,
237
- error: String(error),
238
- operation: 'unsubscribe',
239
- });
240
- await __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_forceReconnection).call(this).catch(() => {
241
- // no-op
242
233
  });
234
+ __classPrivateFieldGet(this, _OHLCVService_channels, "f").delete(channel);
235
+ try {
236
+ const subscriptions = __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:getSubscriptionsByChannel', channel);
237
+ for (const sub of subscriptions) {
238
+ await sub.unsubscribe();
239
+ }
240
+ log('OHLCV-WS: WS unsubscribe completed', { channel });
241
+ }
242
+ catch (error) {
243
+ log('OHLCV-WS: Unsubscription failed, forcing reconnection', {
244
+ channel,
245
+ error,
246
+ });
247
+ __classPrivateFieldGet(this, _OHLCVService_messenger, "f").publish('OHLCVService:subscriptionError', {
248
+ channel,
249
+ error: String(error),
250
+ operation: 'unsubscribe',
251
+ });
252
+ await __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_forceReconnection).call(this).catch(() => {
253
+ // no-op
254
+ });
255
+ }
256
+ }
257
+ finally {
258
+ releaseLock();
243
259
  }
244
260
  }, _OHLCVService_resubscribeActiveChannels =
245
261
  /**
@@ -247,34 +263,43 @@ async function _OHLCVService_performUnsubscribe(channel) {
247
263
  * Called when WebSocket transitions to CONNECTED.
248
264
  */
249
265
  async function _OHLCVService_resubscribeActiveChannels() {
250
- const channelCount = __classPrivateFieldGet(this, _OHLCVService_channels, "f").size;
251
- log('OHLCV-WS: Resubscribing active channels after reconnect', {
252
- count: channelCount,
253
- });
254
- for (const [channel, entry] of __classPrivateFieldGet(this, _OHLCVService_channels, "f").entries()) {
255
- if (entry.refCount === 0) {
256
- continue;
257
- }
258
- try {
259
- if (__classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:channelHasSubscription', channel)) {
260
- log('OHLCV-WS: Channel already subscribed on server, skipping resubscribe', {
266
+ const releaseLock = await __classPrivateFieldGet(this, _OHLCVService_mutex, "f").acquire();
267
+ try {
268
+ const channelCount = __classPrivateFieldGet(this, _OHLCVService_channels, "f").size;
269
+ log('OHLCV-WS: Resubscribing active channels after reconnect', {
270
+ count: channelCount,
271
+ });
272
+ for (const [channel, entry] of __classPrivateFieldGet(this, _OHLCVService_channels, "f").entries()) {
273
+ if (entry.refCount === 0) {
274
+ continue;
275
+ }
276
+ try {
277
+ if (__classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:channelHasSubscription', channel)) {
278
+ log('OHLCV-WS: Channel already subscribed on server, skipping resubscribe', {
279
+ channel,
280
+ });
281
+ continue;
282
+ }
283
+ await __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:subscribe', {
284
+ channels: [channel],
285
+ channelType: SUBSCRIPTION_NAMESPACE,
286
+ callback: (notification) => {
287
+ __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_handleBarUpdate).call(this, channel, notification);
288
+ },
289
+ });
290
+ log('OHLCV-WS: Resubscription succeeded', { channel });
291
+ }
292
+ catch (error) {
293
+ log('OHLCV-WS: Resubscription failed for channel', {
261
294
  channel,
295
+ error,
262
296
  });
263
- continue;
264
297
  }
265
- await __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:subscribe', {
266
- channels: [channel],
267
- channelType: SUBSCRIPTION_NAMESPACE,
268
- callback: (notification) => {
269
- __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_handleBarUpdate).call(this, channel, notification);
270
- },
271
- });
272
- log('OHLCV-WS: Resubscription succeeded', { channel });
273
- }
274
- catch (error) {
275
- log('OHLCV-WS: Resubscription failed for channel', { channel, error });
276
298
  }
277
299
  }
300
+ finally {
301
+ releaseLock();
302
+ }
278
303
  }, _OHLCVService_handleBarUpdate = function _OHLCVService_handleBarUpdate(channel, notification) {
279
304
  const bar = notification.data;
280
305
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -332,27 +357,6 @@ async function _OHLCVService_resubscribeActiveChannels() {
332
357
  }
333
358
  }, _OHLCVService_buildChannel = function _OHLCVService_buildChannel(options) {
334
359
  return `${SUBSCRIPTION_NAMESPACE}.${options.assetId}.${options.interval}.${options.currency}`;
335
- }, _OHLCVService_withChannelLock =
336
- /**
337
- * Serialize async operations for the same channel so that concurrent
338
- * subscribe/unsubscribe calls cannot interleave and corrupt refCount.
339
- * Different channels are not blocked by each other.
340
- *
341
- * @param channel - The channel key to lock on.
342
- * @param fn - The async function to execute under the lock.
343
- */
344
- async function _OHLCVService_withChannelLock(channel, fn) {
345
- const prev = __classPrivateFieldGet(this, _OHLCVService_channelLocks, "f").get(channel) ?? Promise.resolve();
346
- const next = prev.then(fn, fn);
347
- __classPrivateFieldGet(this, _OHLCVService_channelLocks, "f").set(channel, next);
348
- try {
349
- await next;
350
- }
351
- finally {
352
- if (__classPrivateFieldGet(this, _OHLCVService_channelLocks, "f").get(channel) === next) {
353
- __classPrivateFieldGet(this, _OHLCVService_channelLocks, "f").delete(channel);
354
- }
355
- }
356
360
  }, _OHLCVService_forceReconnection = async function _OHLCVService_forceReconnection() {
357
361
  log('OHLCV-WS: Forcing WebSocket reconnection');
358
362
  await __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:forceReconnection');
@@ -1 +1 @@
1
- {"version":3,"file":"OHLCVService.mjs","sourceRoot":"","sources":["../../../src/ws/ohlcv/OHLCVService.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OHLCV Service for real-time candlestick data streaming via WebSocket.\n *\n * Wraps {@link BackendWebSocketService} through the messenger pattern to\n * provide subscribe/unsubscribe semantics for OHLCV market-data channels.\n * Includes reference counting, grace-period unsubscribe, idempotency checks,\n * chain-status forwarding, and automatic resubscription on reconnect.\n */\n\nimport type {\n TraceCallback,\n TraceContext,\n TraceRequest,\n} from '@metamask/controller-utils';\nimport type { Messenger } from '@metamask/messenger';\n\nimport { projectLogger, createModuleLogger } from '../../logger';\nimport type {\n WebSocketConnectionInfo,\n BackendWebSocketServiceConnectionStateChangedEvent,\n ServerNotificationMessage,\n} from '../BackendWebSocketService';\nimport { WebSocketState } from '../BackendWebSocketService';\nimport type { BackendWebSocketServiceMethodActions } from '../BackendWebSocketService-method-action-types';\nimport type { OHLCVServiceMethodActions } from './OHLCVService-method-action-types';\nimport type { OHLCVBar, OHLCVSubscriptionOptions } from './types';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst SERVICE_NAME = 'OHLCVService';\n\nconst log = createModuleLogger(projectLogger, SERVICE_NAME);\n\nconst MESSENGER_EXPOSED_METHODS = ['subscribe', 'unsubscribe'] as const;\n\nconst SUBSCRIPTION_NAMESPACE = 'market-data.v1';\n\nconst SYSTEM_NOTIFICATIONS_CHANNEL = `system-notifications.v1.${SUBSCRIPTION_NAMESPACE}`;\n\n/** Delay before actually unsubscribing from a channel after refCount reaches 0. */\nconst GRACE_PERIOD_MS = 3_000;\n\n// =============================================================================\n// Types — Channel Tracking\n// =============================================================================\n\ntype ChannelEntry = {\n refCount: number;\n gracePeriodTimer?: ReturnType<typeof setTimeout>;\n};\n\n// =============================================================================\n// Types — System Notifications\n// =============================================================================\n\n/**\n * System notification data for chain status updates on market-data channels.\n */\nexport type OHLCVSystemNotificationData = {\n chainIds: string[];\n status: 'down' | 'up';\n timestamp?: number;\n};\n\n// =============================================================================\n// Types — Service Options\n// =============================================================================\n\n/**\n * Configuration options for the OHLCV service.\n */\nexport type OHLCVServiceOptions = {\n /** Optional callback to trace performance of OHLCV operations (default: no-op) */\n traceFn?: TraceCallback;\n};\n\n// =============================================================================\n// Action and Event Types\n// =============================================================================\n\nexport type OHLCVServiceActions = OHLCVServiceMethodActions;\n\nexport const OHLCV_SERVICE_ALLOWED_ACTIONS = [\n 'BackendWebSocketService:connect',\n 'BackendWebSocketService:forceReconnection',\n 'BackendWebSocketService:subscribe',\n 'BackendWebSocketService:getConnectionInfo',\n 'BackendWebSocketService:channelHasSubscription',\n 'BackendWebSocketService:getSubscriptionsByChannel',\n 'BackendWebSocketService:findSubscriptionsByChannelPrefix',\n 'BackendWebSocketService:addChannelCallback',\n 'BackendWebSocketService:removeChannelCallback',\n] as const;\n\nexport const OHLCV_SERVICE_ALLOWED_EVENTS = [\n 'BackendWebSocketService:connectionStateChanged',\n] as const;\n\nexport type AllowedActions = BackendWebSocketServiceMethodActions;\n\n// Events published by OHLCVService\n\nexport type OHLCVServiceBarUpdatedEvent = {\n type: `OHLCVService:barUpdated`;\n payload: [{ channel: string; bar: OHLCVBar }];\n};\n\nexport type OHLCVServiceChainStatusChangedEvent = {\n type: `OHLCVService:chainStatusChanged`;\n payload: [{ chainIds: string[]; status: 'up' | 'down'; timestamp?: number }];\n};\n\nexport type OHLCVServiceSubscriptionErrorEvent = {\n type: `OHLCVService:subscriptionError`;\n payload: [{ channel: string; error: string; operation: string }];\n};\n\nexport type OHLCVServiceEvents =\n | OHLCVServiceBarUpdatedEvent\n | OHLCVServiceChainStatusChangedEvent\n | OHLCVServiceSubscriptionErrorEvent;\n\nexport type AllowedEvents = BackendWebSocketServiceConnectionStateChangedEvent;\n\nexport type OHLCVServiceMessenger = Messenger<\n typeof SERVICE_NAME,\n OHLCVServiceActions | AllowedActions,\n OHLCVServiceEvents | AllowedEvents\n>;\n\n// =============================================================================\n// Main Service Class\n// =============================================================================\n\n/**\n * Service for real-time OHLCV candlestick streaming via the backend WebSocket\n * gateway. Communicates with {@link BackendWebSocketService} exclusively\n * through the messenger — no direct import of the class.\n *\n * Features:\n * - Reference counting: multiple UI consumers share one WebSocket subscription\n * - Grace-period unsubscribe: avoids rapid unsub/resub during navigation\n * - Idempotency: duplicate subscribe calls for the same channel are no-ops\n * - Reconnect resilience: resubscribes all active channels on reconnect\n * - Chain-status forwarding: listens to system-notifications for chain up/down\n *\n */\nexport class OHLCVService {\n readonly name = SERVICE_NAME;\n\n readonly #messenger: OHLCVServiceMessenger;\n\n readonly #trace: TraceCallback;\n\n readonly #channels = new Map<string, ChannelEntry>();\n\n readonly #channelLocks = new Map<string, Promise<void>>();\n\n readonly #chainsUp = new Set<string>();\n\n // =============================================================================\n // Constructor\n // =============================================================================\n\n constructor(\n options: OHLCVServiceOptions & { messenger: OHLCVServiceMessenger },\n ) {\n this.#messenger = options.messenger;\n\n this.#trace =\n options.traceFn ??\n ((<Result>(\n _request: TraceRequest,\n fn?: (context?: TraceContext) => Result,\n ) => fn?.()) as TraceCallback);\n\n this.#messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n\n this.#messenger.subscribe(\n 'BackendWebSocketService:connectionStateChanged',\n // eslint-disable-next-line @typescript-eslint/no-misused-promises\n (connectionInfo: WebSocketConnectionInfo) =>\n this.#handleWebSocketStateChange(connectionInfo),\n );\n }\n\n /**\n * Register the system-notifications channel callback.\n */\n init(): void {\n log('OHLCV-WS: Initializing — registering system-notifications callback');\n this.#messenger.call('BackendWebSocketService:addChannelCallback', {\n channelName: SYSTEM_NOTIFICATIONS_CHANNEL,\n callback: (notification: ServerNotificationMessage) =>\n this.#handleSystemNotification(notification),\n });\n }\n\n // =============================================================================\n // Public — Subscribe / Unsubscribe\n // =============================================================================\n\n /**\n * Subscribe to an OHLCV channel. If this is the first subscriber for the\n * given asset/interval/currency combination a WebSocket subscription is\n * created. Additional calls for the same combination only bump the reference\n * count.\n *\n * @param options - The subscription parameters.\n * @returns A promise that resolves once the subscription is established.\n */\n async subscribe(options: OHLCVSubscriptionOptions): Promise<void> {\n const channel = this.#buildChannel(options);\n return this.#withChannelLock(channel, () => this.#subscribeInner(channel));\n }\n\n async #subscribeInner(channel: string): Promise<void> {\n const entry = this.#channels.get(channel);\n\n if (entry?.gracePeriodTimer) {\n clearTimeout(entry.gracePeriodTimer);\n entry.gracePeriodTimer = undefined;\n entry.refCount += 1;\n log('OHLCV-WS: Cancelled grace-period unsubscribe, bumped refCount', {\n channel,\n refCount: entry.refCount,\n });\n return;\n }\n\n if (entry && entry.refCount > 0) {\n entry.refCount += 1;\n return;\n }\n\n // Flush other channels sitting in grace period to free server-side slots\n // and prevent accumulation when switching time ranges rapidly.\n for (const [otherChannel, otherEntry] of this.#channels.entries()) {\n if (otherChannel !== channel && otherEntry.gracePeriodTimer) {\n clearTimeout(otherEntry.gracePeriodTimer);\n otherEntry.gracePeriodTimer = undefined;\n log('OHLCV-WS: Flushing grace-period channel before new subscribe', {\n flushedChannel: otherChannel,\n newChannel: channel,\n });\n await this.#performUnsubscribe(otherChannel);\n }\n }\n\n try {\n await this.#messenger.call('BackendWebSocketService:connect');\n\n if (\n this.#messenger.call(\n 'BackendWebSocketService:channelHasSubscription',\n channel,\n )\n ) {\n log(\n 'OHLCV-WS: Channel already has WS subscription (idempotency), skipping',\n {\n channel,\n },\n );\n this.#channels.set(channel, { refCount: 1 });\n return;\n }\n\n await this.#messenger.call('BackendWebSocketService:subscribe', {\n channels: [channel],\n channelType: SUBSCRIPTION_NAMESPACE,\n callback: (notification: ServerNotificationMessage) => {\n this.#handleBarUpdate(channel, notification);\n },\n });\n\n this.#channels.set(channel, { refCount: 1 });\n log('OHLCV-WS: Subscribe succeeded — new WS subscription created', {\n channel,\n });\n } catch (error) {\n log('OHLCV-WS: Subscription failed, forcing reconnection', {\n channel,\n error,\n });\n this.#messenger.publish('OHLCVService:subscriptionError', {\n channel,\n error: String(error),\n operation: 'subscribe',\n });\n await this.#forceReconnection();\n }\n }\n\n /**\n * Unsubscribe from an OHLCV channel. Decrements the reference count and,\n * when it reaches zero, starts a grace-period timer before actually\n * unsubscribing from the WebSocket to absorb rapid navigation patterns.\n *\n * @param options - The subscription parameters to unsubscribe from.\n * @returns A promise that resolves once the unsubscription is processed.\n */\n async unsubscribe(options: OHLCVSubscriptionOptions): Promise<void> {\n const channel = this.#buildChannel(options);\n return this.#withChannelLock(channel, () =>\n this.#unsubscribeInner(channel),\n );\n }\n\n async #unsubscribeInner(channel: string): Promise<void> {\n const entry = this.#channels.get(channel);\n\n if (!entry || entry.refCount <= 0) {\n return;\n }\n\n entry.refCount -= 1;\n\n if (entry.refCount > 0) {\n return;\n }\n\n entry.gracePeriodTimer = setTimeout(() => {\n this.#performUnsubscribe(channel).catch(() => {\n // no-op\n });\n }, GRACE_PERIOD_MS);\n }\n\n // =============================================================================\n // Private — WebSocket Subscription Helpers\n // =============================================================================\n\n async #performUnsubscribe(channel: string): Promise<void> {\n log('OHLCV-WS: Grace period expired — performing actual WS unsubscribe', {\n channel,\n });\n this.#channels.delete(channel);\n\n try {\n const subscriptions = this.#messenger.call(\n 'BackendWebSocketService:getSubscriptionsByChannel',\n channel,\n );\n\n for (const sub of subscriptions) {\n await sub.unsubscribe();\n }\n log('OHLCV-WS: WS unsubscribe completed', { channel });\n } catch (error) {\n log('OHLCV-WS: Unsubscription failed, forcing reconnection', {\n channel,\n error,\n });\n this.#messenger.publish('OHLCVService:subscriptionError', {\n channel,\n error: String(error),\n operation: 'unsubscribe',\n });\n await this.#forceReconnection().catch(() => {\n // no-op\n });\n }\n }\n\n /**\n * Resubscribe all channels that were active before a disconnect.\n * Called when WebSocket transitions to CONNECTED.\n */\n async #resubscribeActiveChannels(): Promise<void> {\n const channelCount = this.#channels.size;\n log('OHLCV-WS: Resubscribing active channels after reconnect', {\n count: channelCount,\n });\n\n for (const [channel, entry] of this.#channels.entries()) {\n if (entry.refCount === 0) {\n continue;\n }\n\n try {\n if (\n this.#messenger.call(\n 'BackendWebSocketService:channelHasSubscription',\n channel,\n )\n ) {\n log(\n 'OHLCV-WS: Channel already subscribed on server, skipping resubscribe',\n {\n channel,\n },\n );\n continue;\n }\n\n await this.#messenger.call('BackendWebSocketService:subscribe', {\n channels: [channel],\n channelType: SUBSCRIPTION_NAMESPACE,\n callback: (notification: ServerNotificationMessage) => {\n this.#handleBarUpdate(channel, notification);\n },\n });\n log('OHLCV-WS: Resubscription succeeded', { channel });\n } catch (error) {\n log('OHLCV-WS: Resubscription failed for channel', { channel, error });\n }\n }\n }\n\n // =============================================================================\n // Private — Message Handlers\n // =============================================================================\n\n #handleBarUpdate(\n channel: string,\n notification: ServerNotificationMessage,\n ): void {\n const bar = notification.data as OHLCVBar;\n\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.#trace(\n {\n name: `${SERVICE_NAME} Bar Update`,\n data: { channel, timestamp: bar.timestamp },\n tags: { service: SERVICE_NAME },\n },\n () => {\n this.#messenger.publish('OHLCVService:barUpdated', { channel, bar });\n },\n );\n }\n\n #handleSystemNotification(notification: ServerNotificationMessage): void {\n const data = notification.data as OHLCVSystemNotificationData;\n const { timestamp } = notification;\n\n if (!data.chainIds || !Array.isArray(data.chainIds) || !data.status) {\n throw new Error(\n 'Invalid system notification data: missing chainIds or status',\n );\n }\n\n if (data.status === 'up') {\n for (const chainId of data.chainIds) {\n this.#chainsUp.add(chainId);\n }\n } else {\n for (const chainId of data.chainIds) {\n this.#chainsUp.delete(chainId);\n }\n }\n\n this.#messenger.publish('OHLCVService:chainStatusChanged', {\n chainIds: data.chainIds,\n status: data.status,\n timestamp,\n });\n\n log(`OHLCV-WS: Chain status change: ${data.status}`, {\n chains: data.chainIds,\n status: data.status,\n });\n }\n\n async #handleWebSocketStateChange(\n connectionInfo: WebSocketConnectionInfo,\n ): Promise<void> {\n const { state } = connectionInfo;\n\n if (state === WebSocketState.CONNECTED) {\n await this.#resubscribeActiveChannels();\n } else if (state === WebSocketState.DISCONNECTED) {\n const chainsToMarkDown = Array.from(this.#chainsUp);\n\n if (chainsToMarkDown.length > 0) {\n this.#messenger.publish('OHLCVService:chainStatusChanged', {\n chainIds: chainsToMarkDown,\n status: 'down',\n timestamp: Date.now(),\n });\n\n log(\n 'OHLCV-WS: WebSocket disconnection — marked tracked chains as down',\n {\n count: chainsToMarkDown.length,\n chains: chainsToMarkDown,\n },\n );\n\n this.#chainsUp.clear();\n }\n }\n }\n\n // =============================================================================\n // Private — Utility\n // =============================================================================\n\n #buildChannel(options: OHLCVSubscriptionOptions): string {\n return `${SUBSCRIPTION_NAMESPACE}.${options.assetId}.${options.interval}.${options.currency}`;\n }\n\n /**\n * Serialize async operations for the same channel so that concurrent\n * subscribe/unsubscribe calls cannot interleave and corrupt refCount.\n * Different channels are not blocked by each other.\n *\n * @param channel - The channel key to lock on.\n * @param fn - The async function to execute under the lock.\n */\n async #withChannelLock(\n channel: string,\n fn: () => Promise<void>,\n ): Promise<void> {\n const prev = this.#channelLocks.get(channel) ?? Promise.resolve();\n const next = prev.then(fn, fn);\n this.#channelLocks.set(channel, next);\n try {\n await next;\n } finally {\n if (this.#channelLocks.get(channel) === next) {\n this.#channelLocks.delete(channel);\n }\n }\n }\n\n async #forceReconnection(): Promise<void> {\n log('OHLCV-WS: Forcing WebSocket reconnection');\n await this.#messenger.call('BackendWebSocketService:forceReconnection');\n }\n\n // =============================================================================\n // Public — Cleanup\n // =============================================================================\n\n /**\n * Destroy the service and clean up all resources.\n */\n destroy(): void {\n for (const entry of this.#channels.values()) {\n if (entry.gracePeriodTimer) {\n clearTimeout(entry.gracePeriodTimer);\n }\n }\n this.#channels.clear();\n this.#channelLocks.clear();\n\n this.#messenger.call(\n 'BackendWebSocketService:removeChannelCallback',\n SYSTEM_NOTIFICATIONS_CHANNEL,\n );\n }\n}\n"]}
1
+ {"version":3,"file":"OHLCVService.mjs","sourceRoot":"","sources":["../../../src/ws/ohlcv/OHLCVService.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OHLCV Service for real-time candlestick data streaming via WebSocket.\n *\n * Wraps {@link BackendWebSocketService} through the messenger pattern to\n * provide subscribe/unsubscribe semantics for OHLCV market-data channels.\n * Includes reference counting, grace-period unsubscribe, idempotency checks,\n * chain-status forwarding, and automatic resubscription on reconnect.\n */\n\nimport type {\n TraceCallback,\n TraceContext,\n TraceRequest,\n} from '@metamask/controller-utils';\nimport type { Messenger } from '@metamask/messenger';\nimport { Mutex } from 'async-mutex';\n\nimport { projectLogger, createModuleLogger } from '../../logger';\nimport type {\n WebSocketConnectionInfo,\n BackendWebSocketServiceConnectionStateChangedEvent,\n ServerNotificationMessage,\n} from '../BackendWebSocketService';\nimport { WebSocketState } from '../BackendWebSocketService';\nimport type { BackendWebSocketServiceMethodActions } from '../BackendWebSocketService-method-action-types';\nimport type { OHLCVServiceMethodActions } from './OHLCVService-method-action-types';\nimport type { OHLCVBar, OHLCVSubscriptionOptions } from './types';\n\n// =============================================================================\n// Constants\n// =============================================================================\n\nconst SERVICE_NAME = 'OHLCVService';\n\nconst log = createModuleLogger(projectLogger, SERVICE_NAME);\n\nconst MESSENGER_EXPOSED_METHODS = ['subscribe', 'unsubscribe'] as const;\n\nconst SUBSCRIPTION_NAMESPACE = 'market-data.v1';\n\nconst SYSTEM_NOTIFICATIONS_CHANNEL = `system-notifications.v1.${SUBSCRIPTION_NAMESPACE}`;\n\n/** Delay before actually unsubscribing from a channel after refCount reaches 0. */\nconst GRACE_PERIOD_MS = 3_000;\n\n// =============================================================================\n// Types — Channel Tracking\n// =============================================================================\n\ntype ChannelEntry = {\n refCount: number;\n gracePeriodTimer?: ReturnType<typeof setTimeout>;\n};\n\n// =============================================================================\n// Types — System Notifications\n// =============================================================================\n\n/**\n * System notification data for chain status updates on market-data channels.\n */\nexport type OHLCVSystemNotificationData = {\n chainIds: string[];\n status: 'down' | 'up';\n timestamp?: number;\n};\n\n// =============================================================================\n// Types — Service Options\n// =============================================================================\n\n/**\n * Configuration options for the OHLCV service.\n */\nexport type OHLCVServiceOptions = {\n /** Optional callback to trace performance of OHLCV operations (default: no-op) */\n traceFn?: TraceCallback;\n};\n\n// =============================================================================\n// Action and Event Types\n// =============================================================================\n\nexport type OHLCVServiceActions = OHLCVServiceMethodActions;\n\nexport const OHLCV_SERVICE_ALLOWED_ACTIONS = [\n 'BackendWebSocketService:connect',\n 'BackendWebSocketService:forceReconnection',\n 'BackendWebSocketService:subscribe',\n 'BackendWebSocketService:getConnectionInfo',\n 'BackendWebSocketService:channelHasSubscription',\n 'BackendWebSocketService:getSubscriptionsByChannel',\n 'BackendWebSocketService:findSubscriptionsByChannelPrefix',\n 'BackendWebSocketService:addChannelCallback',\n 'BackendWebSocketService:removeChannelCallback',\n] as const;\n\nexport const OHLCV_SERVICE_ALLOWED_EVENTS = [\n 'BackendWebSocketService:connectionStateChanged',\n] as const;\n\nexport type AllowedActions = BackendWebSocketServiceMethodActions;\n\n// Events published by OHLCVService\n\nexport type OHLCVServiceBarUpdatedEvent = {\n type: `OHLCVService:barUpdated`;\n payload: [{ channel: string; bar: OHLCVBar }];\n};\n\nexport type OHLCVServiceChainStatusChangedEvent = {\n type: `OHLCVService:chainStatusChanged`;\n payload: [{ chainIds: string[]; status: 'up' | 'down'; timestamp?: number }];\n};\n\nexport type OHLCVServiceSubscriptionErrorEvent = {\n type: `OHLCVService:subscriptionError`;\n payload: [{ channel: string; error: string; operation: string }];\n};\n\nexport type OHLCVServiceEvents =\n | OHLCVServiceBarUpdatedEvent\n | OHLCVServiceChainStatusChangedEvent\n | OHLCVServiceSubscriptionErrorEvent;\n\nexport type AllowedEvents = BackendWebSocketServiceConnectionStateChangedEvent;\n\nexport type OHLCVServiceMessenger = Messenger<\n typeof SERVICE_NAME,\n OHLCVServiceActions | AllowedActions,\n OHLCVServiceEvents | AllowedEvents\n>;\n\n// =============================================================================\n// Main Service Class\n// =============================================================================\n\n/**\n * Service for real-time OHLCV candlestick streaming via the backend WebSocket\n * gateway. Communicates with {@link BackendWebSocketService} exclusively\n * through the messenger — no direct import of the class.\n *\n * Features:\n * - Reference counting: multiple UI consumers share one WebSocket subscription\n * - Grace-period unsubscribe: avoids rapid unsub/resub during navigation\n * - Idempotency: duplicate subscribe calls for the same channel are no-ops\n * - Reconnect resilience: resubscribes all active channels on reconnect\n * - Chain-status forwarding: listens to system-notifications for chain up/down\n *\n */\nexport class OHLCVService {\n readonly name = SERVICE_NAME;\n\n readonly #messenger: OHLCVServiceMessenger;\n\n readonly #trace: TraceCallback;\n\n readonly #channels = new Map<string, ChannelEntry>();\n\n readonly #mutex = new Mutex();\n\n readonly #chainsUp = new Set<string>();\n\n // =============================================================================\n // Constructor\n // =============================================================================\n\n constructor(\n options: OHLCVServiceOptions & { messenger: OHLCVServiceMessenger },\n ) {\n this.#messenger = options.messenger;\n\n this.#trace =\n options.traceFn ??\n ((<Result>(\n _request: TraceRequest,\n fn?: (context?: TraceContext) => Result,\n ) => fn?.()) as TraceCallback);\n\n this.#messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n\n this.#messenger.subscribe(\n 'BackendWebSocketService:connectionStateChanged',\n // eslint-disable-next-line @typescript-eslint/no-misused-promises\n (connectionInfo: WebSocketConnectionInfo) =>\n this.#handleWebSocketStateChange(connectionInfo),\n );\n }\n\n /**\n * Register the system-notifications channel callback.\n */\n init(): void {\n log('OHLCV-WS: Initializing — registering system-notifications callback');\n this.#messenger.call('BackendWebSocketService:addChannelCallback', {\n channelName: SYSTEM_NOTIFICATIONS_CHANNEL,\n callback: (notification: ServerNotificationMessage) =>\n this.#handleSystemNotification(notification),\n });\n }\n\n // =============================================================================\n // Public — Subscribe / Unsubscribe\n // =============================================================================\n\n /**\n * Subscribe to an OHLCV channel. If this is the first subscriber for the\n * given asset/interval/currency combination a WebSocket subscription is\n * created. Additional calls for the same combination only bump the reference\n * count.\n *\n * @param options - The subscription parameters.\n * @returns A promise that resolves once the subscription is established.\n */\n async subscribe(options: OHLCVSubscriptionOptions): Promise<void> {\n const channel = this.#buildChannel(options);\n const releaseLock = await this.#mutex.acquire();\n try {\n await this.#subscribeInner(channel);\n } finally {\n releaseLock();\n }\n }\n\n async #subscribeInner(channel: string): Promise<void> {\n const entry = this.#channels.get(channel);\n\n if (entry?.gracePeriodTimer) {\n clearTimeout(entry.gracePeriodTimer);\n entry.gracePeriodTimer = undefined;\n entry.refCount += 1;\n log('OHLCV-WS: Cancelled grace-period unsubscribe, bumped refCount', {\n channel,\n refCount: entry.refCount,\n });\n\n if (\n this.#messenger.call(\n 'BackendWebSocketService:channelHasSubscription',\n channel,\n )\n ) {\n return;\n }\n // WS subscription was lost (e.g. after disconnect/reconnect) — fall\n // through to recreate it while keeping the already-bumped refCount.\n } else if (entry && entry.refCount > 0) {\n entry.refCount += 1;\n return;\n }\n\n try {\n await this.#messenger.call('BackendWebSocketService:connect');\n\n if (\n this.#messenger.call(\n 'BackendWebSocketService:channelHasSubscription',\n channel,\n )\n ) {\n log(\n 'OHLCV-WS: Channel already has WS subscription (idempotency), skipping',\n {\n channel,\n },\n );\n this.#channels.set(channel, { refCount: 1 });\n return;\n }\n\n await this.#messenger.call('BackendWebSocketService:subscribe', {\n channels: [channel],\n channelType: SUBSCRIPTION_NAMESPACE,\n callback: (notification: ServerNotificationMessage) => {\n this.#handleBarUpdate(channel, notification);\n },\n });\n\n this.#channels.set(channel, { refCount: 1 });\n log('OHLCV-WS: Subscribe succeeded — new WS subscription created', {\n channel,\n });\n } catch (error) {\n log('OHLCV-WS: Subscription failed, forcing reconnection', {\n channel,\n error,\n });\n this.#messenger.publish('OHLCVService:subscriptionError', {\n channel,\n error: String(error),\n operation: 'subscribe',\n });\n await this.#forceReconnection();\n }\n }\n\n /**\n * Unsubscribe from an OHLCV channel. Decrements the reference count and,\n * when it reaches zero, starts a grace-period timer before actually\n * unsubscribing from the WebSocket to absorb rapid navigation patterns.\n *\n * @param options - The subscription parameters to unsubscribe from.\n * @returns A promise that resolves once the unsubscription is processed.\n */\n async unsubscribe(options: OHLCVSubscriptionOptions): Promise<void> {\n const channel = this.#buildChannel(options);\n const releaseLock = await this.#mutex.acquire();\n try {\n await this.#unsubscribeInner(channel);\n } finally {\n releaseLock();\n }\n }\n\n async #unsubscribeInner(channel: string): Promise<void> {\n const entry = this.#channels.get(channel);\n\n if (!entry || entry.refCount <= 0) {\n return;\n }\n\n entry.refCount -= 1;\n\n if (entry.refCount > 0) {\n return;\n }\n\n entry.gracePeriodTimer = setTimeout(() => {\n entry.gracePeriodTimer = undefined;\n this.#performUnsubscribe(channel).catch(() => {\n // no-op\n });\n }, GRACE_PERIOD_MS);\n }\n\n // =============================================================================\n // Private — WebSocket Subscription Helpers\n // =============================================================================\n\n async #performUnsubscribe(channel: string): Promise<void> {\n const releaseLock = await this.#mutex.acquire();\n try {\n const entry = this.#channels.get(channel);\n if (entry && entry.refCount > 0) {\n log(\n 'OHLCV-WS: Skipping unsubscribe — new subscriber arrived while queued',\n { channel, refCount: entry.refCount },\n );\n return;\n }\n\n log('OHLCV-WS: Grace period expired — performing actual WS unsubscribe', {\n channel,\n });\n this.#channels.delete(channel);\n\n try {\n const subscriptions = this.#messenger.call(\n 'BackendWebSocketService:getSubscriptionsByChannel',\n channel,\n );\n\n for (const sub of subscriptions) {\n await sub.unsubscribe();\n }\n log('OHLCV-WS: WS unsubscribe completed', { channel });\n } catch (error) {\n log('OHLCV-WS: Unsubscription failed, forcing reconnection', {\n channel,\n error,\n });\n this.#messenger.publish('OHLCVService:subscriptionError', {\n channel,\n error: String(error),\n operation: 'unsubscribe',\n });\n await this.#forceReconnection().catch(() => {\n // no-op\n });\n }\n } finally {\n releaseLock();\n }\n }\n\n /**\n * Resubscribe all channels that were active before a disconnect.\n * Called when WebSocket transitions to CONNECTED.\n */\n async #resubscribeActiveChannels(): Promise<void> {\n const releaseLock = await this.#mutex.acquire();\n try {\n const channelCount = this.#channels.size;\n log('OHLCV-WS: Resubscribing active channels after reconnect', {\n count: channelCount,\n });\n\n for (const [channel, entry] of this.#channels.entries()) {\n if (entry.refCount === 0) {\n continue;\n }\n\n try {\n if (\n this.#messenger.call(\n 'BackendWebSocketService:channelHasSubscription',\n channel,\n )\n ) {\n log(\n 'OHLCV-WS: Channel already subscribed on server, skipping resubscribe',\n {\n channel,\n },\n );\n continue;\n }\n\n await this.#messenger.call('BackendWebSocketService:subscribe', {\n channels: [channel],\n channelType: SUBSCRIPTION_NAMESPACE,\n callback: (notification: ServerNotificationMessage) => {\n this.#handleBarUpdate(channel, notification);\n },\n });\n log('OHLCV-WS: Resubscription succeeded', { channel });\n } catch (error) {\n log('OHLCV-WS: Resubscription failed for channel', {\n channel,\n error,\n });\n }\n }\n } finally {\n releaseLock();\n }\n }\n\n // =============================================================================\n // Private — Message Handlers\n // =============================================================================\n\n #handleBarUpdate(\n channel: string,\n notification: ServerNotificationMessage,\n ): void {\n const bar = notification.data as OHLCVBar;\n\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.#trace(\n {\n name: `${SERVICE_NAME} Bar Update`,\n data: { channel, timestamp: bar.timestamp },\n tags: { service: SERVICE_NAME },\n },\n () => {\n this.#messenger.publish('OHLCVService:barUpdated', { channel, bar });\n },\n );\n }\n\n #handleSystemNotification(notification: ServerNotificationMessage): void {\n const data = notification.data as OHLCVSystemNotificationData;\n const { timestamp } = notification;\n\n if (!data.chainIds || !Array.isArray(data.chainIds) || !data.status) {\n throw new Error(\n 'Invalid system notification data: missing chainIds or status',\n );\n }\n\n if (data.status === 'up') {\n for (const chainId of data.chainIds) {\n this.#chainsUp.add(chainId);\n }\n } else {\n for (const chainId of data.chainIds) {\n this.#chainsUp.delete(chainId);\n }\n }\n\n this.#messenger.publish('OHLCVService:chainStatusChanged', {\n chainIds: data.chainIds,\n status: data.status,\n timestamp,\n });\n\n log(`OHLCV-WS: Chain status change: ${data.status}`, {\n chains: data.chainIds,\n status: data.status,\n });\n }\n\n async #handleWebSocketStateChange(\n connectionInfo: WebSocketConnectionInfo,\n ): Promise<void> {\n const { state } = connectionInfo;\n\n if (state === WebSocketState.CONNECTED) {\n await this.#resubscribeActiveChannels();\n } else if (state === WebSocketState.DISCONNECTED) {\n const chainsToMarkDown = Array.from(this.#chainsUp);\n\n if (chainsToMarkDown.length > 0) {\n this.#messenger.publish('OHLCVService:chainStatusChanged', {\n chainIds: chainsToMarkDown,\n status: 'down',\n timestamp: Date.now(),\n });\n\n log(\n 'OHLCV-WS: WebSocket disconnection — marked tracked chains as down',\n {\n count: chainsToMarkDown.length,\n chains: chainsToMarkDown,\n },\n );\n\n this.#chainsUp.clear();\n }\n }\n }\n\n // =============================================================================\n // Private — Utility\n // =============================================================================\n\n #buildChannel(options: OHLCVSubscriptionOptions): string {\n return `${SUBSCRIPTION_NAMESPACE}.${options.assetId}.${options.interval}.${options.currency}`;\n }\n\n async #forceReconnection(): Promise<void> {\n log('OHLCV-WS: Forcing WebSocket reconnection');\n await this.#messenger.call('BackendWebSocketService:forceReconnection');\n }\n\n // =============================================================================\n // Public — Cleanup\n // =============================================================================\n\n /**\n * Destroy the service and clean up all resources.\n */\n destroy(): void {\n for (const entry of this.#channels.values()) {\n if (entry.gracePeriodTimer) {\n clearTimeout(entry.gracePeriodTimer);\n }\n }\n this.#channels.clear();\n this.#chainsUp.clear();\n\n this.#messenger.call(\n 'BackendWebSocketService:removeChannelCallback',\n SYSTEM_NOTIFICATIONS_CHANNEL,\n );\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask-previews/core-backend",
3
- "version": "6.2.2-preview-c7e584108",
3
+ "version": "6.2.2-preview-337abd924",
4
4
  "description": "Core backend services for MetaMask",
5
5
  "keywords": [
6
6
  "Ethereum",
@@ -53,13 +53,14 @@
53
53
  "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
54
54
  },
55
55
  "dependencies": {
56
- "@metamask/accounts-controller": "^38.1.0",
57
- "@metamask/controller-utils": "^12.0.0",
56
+ "@metamask/accounts-controller": "^38.1.1",
57
+ "@metamask/controller-utils": "^12.1.0",
58
58
  "@metamask/keyring-controller": "^25.5.0",
59
59
  "@metamask/messenger": "^1.2.0",
60
60
  "@metamask/profile-sync-controller": "^28.0.2",
61
61
  "@metamask/utils": "^11.9.0",
62
62
  "@tanstack/query-core": "^5.62.16",
63
+ "async-mutex": "^0.5.0",
63
64
  "uuid": "^8.3.2"
64
65
  },
65
66
  "devDependencies": {