From 21d7becc068e0bf3e75fa277bd5e0bc9e2a7f036 Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Wed, 16 Jul 2025 21:11:15 -0600 Subject: [PATCH 1/2] remove heartbeat worker --- .../relay/connection/HeartbeatWorker.js | 62 ----- .../relay/connection/HeartbeatWorker.test.ts | 234 ------------------ 2 files changed, 296 deletions(-) delete mode 100644 packages/wallet-sdk/src/sign/walletlink/relay/connection/HeartbeatWorker.js delete mode 100644 packages/wallet-sdk/src/sign/walletlink/relay/connection/HeartbeatWorker.test.ts diff --git a/packages/wallet-sdk/src/sign/walletlink/relay/connection/HeartbeatWorker.js b/packages/wallet-sdk/src/sign/walletlink/relay/connection/HeartbeatWorker.js deleted file mode 100644 index f772671838..0000000000 --- a/packages/wallet-sdk/src/sign/walletlink/relay/connection/HeartbeatWorker.js +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2018-2025 Coinbase, Inc. - -/** - * This worker is used to send heartbeat messages to the main thread. - * It is used to keep the websocket connection alive when the webpage is backgrounded. - * - */ - -// Define the heartbeat interval constant directly in the worker to avoid import issues -const HEARTBEAT_INTERVAL = 10000; - -let heartbeatInterval; - -// Listen for messages from the main thread -self.addEventListener('message', (event) => { - const { type } = event.data; - - switch (type) { - case 'start': - startHeartbeat(); - break; - case 'stop': - stopHeartbeat(); - break; - default: - console.warn('Unknown message type received by HeartbeatWorker:', type); - } -}); - -function startHeartbeat() { - // Clear any existing interval - if (heartbeatInterval) { - clearInterval(heartbeatInterval); - } - - // Start the heartbeat interval - heartbeatInterval = setInterval(() => { - // Send heartbeat message to main thread - const response = { type: 'heartbeat' }; - self.postMessage(response); - }, HEARTBEAT_INTERVAL); - - // Send confirmation that heartbeat started - const response = { type: 'started' }; - self.postMessage(response); -} - -function stopHeartbeat() { - if (heartbeatInterval) { - clearInterval(heartbeatInterval); - heartbeatInterval = undefined; - } - - // Send confirmation that heartbeat stopped - const response = { type: 'stopped' }; - self.postMessage(response); -} - -// Handle worker termination -self.addEventListener('beforeunload', () => { - stopHeartbeat(); -}); \ No newline at end of file diff --git a/packages/wallet-sdk/src/sign/walletlink/relay/connection/HeartbeatWorker.test.ts b/packages/wallet-sdk/src/sign/walletlink/relay/connection/HeartbeatWorker.test.ts deleted file mode 100644 index 026ce2fd4a..0000000000 --- a/packages/wallet-sdk/src/sign/walletlink/relay/connection/HeartbeatWorker.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -import '@vitest/web-worker'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -describe('HeartbeatWorker', () => { - let worker: Worker; - - beforeEach(async () => { - // Create a new worker instance for each test - worker = new Worker(new URL('./HeartbeatWorker.js', import.meta.url)); - }); - - afterEach(() => { - if (worker) { - worker.terminate(); - } - }); - - describe('Message Handling', () => { - it('should start heartbeat and send confirmation', async () => { - const messagePromise = new Promise((resolve) => { - worker.addEventListener('message', resolve, { once: true }); - }); - - worker.postMessage({ type: 'start' }); - - const event = await messagePromise; - expect(event.data).toEqual({ type: 'started' }); - }); - - it('should send heartbeat messages at regular intervals', async () => { - worker.postMessage({ type: 'start' }); - - await new Promise((resolve) => { - worker.addEventListener('message', (event) => { - if (event.data.type === 'started') { - resolve(); - } - }, { once: true }); - }); - - const heartbeats: MessageEvent[] = []; - const heartbeatPromise = new Promise((resolve) => { - let count = 0; - worker.addEventListener('message', (event) => { - if (event.data.type === 'heartbeat') { - heartbeats.push(event); - count++; - if (count >= 2) { - resolve(); - } - } - }); - }); - - // Wait for at least 2 heartbeat messages (this will take ~20 seconds in real time) - // For testing, we'll use a shorter timeout and verify the structure - await Promise.race([ - heartbeatPromise, - new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout waiting for heartbeats')), 25000)) - ]); - - expect(heartbeats.length).toBeGreaterThanOrEqual(2); - heartbeats.forEach(event => { - expect(event.data).toEqual({ type: 'heartbeat' }); - }); - }, 30000); // 30 second timeout for this test - - it('should stop heartbeat and send confirmation', async () => { - worker.postMessage({ type: 'start' }); - - await new Promise((resolve) => { - worker.addEventListener('message', (event) => { - if (event.data.type === 'started') { - resolve(); - } - }, { once: true }); - }); - - const stopPromise = new Promise((resolve) => { - worker.addEventListener('message', (event) => { - if (event.data.type === 'stopped') { - resolve(event); - } - }, { once: true }); - }); - - worker.postMessage({ type: 'stop' }); - - const event = await stopPromise; - expect(event.data).toEqual({ type: 'stopped' }); - }); - - it('should handle unknown message types gracefully', async () => { - const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - worker.postMessage({ type: 'unknown' }); - - // Give the worker time to process the message - await new Promise(resolve => setTimeout(resolve, 100)); - - // Note: We can't directly verify console.warn was called in the worker context - // but we can verify the worker doesn't crash or send unexpected messages - - const messagePromise = new Promise((resolve) => { - worker.addEventListener('message', resolve, { once: true }); - }); - - worker.postMessage({ type: 'start' }); - const event = await messagePromise; - expect(event.data).toEqual({ type: 'started' }); - - consoleSpy.mockRestore(); - }); - }); - - describe('Heartbeat Interval Management', () => { - it('should handle restart without issues', async () => { - worker.postMessage({ type: 'start' }); - - await new Promise((resolve) => { - worker.addEventListener('message', (event) => { - if (event.data.type === 'started') { - resolve(); - } - }, { once: true }); - }); - - // Start again (should clear previous interval) - const secondStartPromise = new Promise((resolve) => { - worker.addEventListener('message', (event) => { - if (event.data.type === 'started') { - resolve(event); - } - }, { once: true }); - }); - - worker.postMessage({ type: 'start' }); - const event = await secondStartPromise; - expect(event.data).toEqual({ type: 'started' }); - }); - - it('should stop cleanly even when no heartbeat is running', async () => { - const stopPromise = new Promise((resolve) => { - worker.addEventListener('message', resolve, { once: true }); - }); - - // Stop without starting first - worker.postMessage({ type: 'stop' }); - - const event = await stopPromise; - expect(event.data).toEqual({ type: 'stopped' }); - }); - }); - - describe('Message Flow', () => { - it('should handle complete start-heartbeat-stop cycle', async () => { - const messages: any[] = []; - - worker.addEventListener('message', (event) => { - messages.push(event.data); - }); - - worker.postMessage({ type: 'start' }); - - await new Promise((resolve) => { - const checkMessages = () => { - if (messages.some(msg => msg.type === 'started')) { - resolve(); - } else { - setTimeout(checkMessages, 10); - } - }; - checkMessages(); - }); - - await new Promise((resolve) => { - const checkMessages = () => { - if (messages.some(msg => msg.type === 'heartbeat')) { - resolve(); - } else { - setTimeout(checkMessages, 100); - } - }; - checkMessages(); - }); - - worker.postMessage({ type: 'stop' }); - - await new Promise((resolve) => { - const checkMessages = () => { - if (messages.some(msg => msg.type === 'stopped')) { - resolve(); - } else { - setTimeout(checkMessages, 10); - } - }; - checkMessages(); - }); - - // Verify we got all expected message types - expect(messages.some(msg => msg.type === 'started')).toBe(true); - expect(messages.some(msg => msg.type === 'heartbeat')).toBe(true); - expect(messages.some(msg => msg.type === 'stopped')).toBe(true); - }, 15000); // 15 second timeout - - it('should use correct heartbeat interval timing', async () => { - const heartbeatTimes: number[] = []; - - worker.addEventListener('message', (event) => { - if (event.data.type === 'heartbeat') { - heartbeatTimes.push(Date.now()); - } - }); - - worker.postMessage({ type: 'start' }); - - await new Promise((resolve) => { - const checkHeartbeats = () => { - if (heartbeatTimes.length >= 2) { - resolve(); - } else { - setTimeout(checkHeartbeats, 100); - } - }; - checkHeartbeats(); - }); - - // Verify the interval is approximately 10 seconds (allow some tolerance) - const interval = heartbeatTimes[1] - heartbeatTimes[0]; - expect(interval).toBeGreaterThan(9500); // 9.5 seconds minimum - expect(interval).toBeLessThan(10500); // 10.5 seconds maximum - }, 25000); // 25 second timeout - }); -}); \ No newline at end of file From 6f2a47f790f09e25db7a959319a9688005e9e8b8 Mon Sep 17 00:00:00 2001 From: Spencer Stock Date: Mon, 21 Jul 2025 13:55:49 -0600 Subject: [PATCH 2/2] fix CI --- packages/wallet-sdk/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/wallet-sdk/src/index.ts b/packages/wallet-sdk/src/index.ts index 2a57b93a63..348920b71b 100644 --- a/packages/wallet-sdk/src/index.ts +++ b/packages/wallet-sdk/src/index.ts @@ -2,8 +2,9 @@ import { CoinbaseWalletSDK } from './CoinbaseWalletSDK.js'; export default CoinbaseWalletSDK; -export type { AppMetadata, Preference, ProviderInterface } from ':core/provider/interface.js'; +export type { AppMetadata, Preference, ProviderInterface, SubAccountOptions } from ':core/provider/interface.js'; export type { CoinbaseWalletProvider } from './CoinbaseWalletProvider.js'; export { CoinbaseWalletSDK } from './CoinbaseWalletSDK.js'; export { createCoinbaseWalletSDK } from './createCoinbaseWalletSDK.js'; export { getCryptoKeyAccount, removeCryptoKey } from './kms/crypto-key/index.js'; +