diff --git a/.vscode/settings.json b/.vscode/settings.json index 84d27e7..6fe1eed 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "Algo", "Authed", "Authorise", + "Debounces", "Inequal", "KRISTWALLET", "KRISTWALLETEXTENSION", diff --git a/package.json b/package.json index 8e239c7..aed7805 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "i18next": "^19.7.0", "i18next-browser-languagedetector": "^6.0.1", "i18next-http-backend": "^1.0.20", + "lodash.throttle": "^4.1.1", "react": "^17.0.1", "react-dom": "^17.0.1", "react-i18next": "^11.8.6", @@ -67,6 +68,7 @@ "@types/debug": "^4.1.5", "@types/file-saver": "^2.0.1", "@types/jest": "^26.0.20", + "@types/lodash.throttle": "^4.1.6", "@types/node": "^12.19.16", "@types/react": "^17.0.1", "@types/react-dom": "^17.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 364322f..9c1b819 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,7 @@ i18next: 19.8.7 i18next-browser-languagedetector: 6.0.1 i18next-http-backend: 1.1.0 + lodash.throttle: 4.1.1 react: 17.0.1 react-dom: 17.0.1_react@17.0.1 react-i18next: 11.8.6_i18next@19.8.7+react@17.0.1 @@ -30,6 +31,7 @@ '@types/debug': 4.1.5 '@types/file-saver': 2.0.1 '@types/jest': 26.0.20 + '@types/lodash.throttle': 4.1.6 '@types/node': 12.20.0 '@types/react': 17.0.2 '@types/react-dom': 17.0.1 @@ -2065,6 +2067,16 @@ dev: true resolution: integrity: sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + /@types/lodash.throttle/4.1.6: + dependencies: + '@types/lodash': 4.14.168 + dev: true + resolution: + integrity: sha512-/UIH96i/sIRYGC60NoY72jGkCJtFN5KVPhEMMMTjol65effe1gPn0tycJqV5tlSwMTzX8FqzB5yAj0rfGHTPNg== + /@types/lodash/4.14.168: + dev: true + resolution: + integrity: sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q== /@types/minimatch/3.0.3: dev: true resolution: @@ -7838,6 +7850,10 @@ dev: true resolution: integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + /lodash.throttle/4.1.1: + dev: false + resolution: + integrity: sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= /lodash.uniq/4.5.0: dev: true resolution: @@ -13365,6 +13381,7 @@ '@types/debug': ^4.1.5 '@types/file-saver': ^2.0.1 '@types/jest': ^26.0.20 + '@types/lodash.throttle': ^4.1.6 '@types/node': ^12.19.16 '@types/react': ^17.0.1 '@types/react-dom': ^17.0.0 @@ -13389,6 +13406,7 @@ i18next: ^19.7.0 i18next-browser-languagedetector: ^6.0.1 i18next-http-backend: ^1.0.20 + lodash.throttle: ^4.1.1 prettier: ^2.2.1 react: ^17.0.1 react-dom: ^17.0.1 diff --git a/src/components/ws/WebsocketService.tsx b/src/components/ws/WebsocketService.tsx index f666b30..ce3affd 100644 --- a/src/components/ws/WebsocketService.tsx +++ b/src/components/ws/WebsocketService.tsx @@ -12,22 +12,29 @@ import { findWalletByAddress, syncWalletUpdate } from "../../krist/wallets/Wallet"; import WebSocketAsPromised from "websocket-as-promised"; +import throttle from "lodash.throttle"; + import Debug from "debug"; import { useMountEffect } from "../../utils"; const debug = Debug("kristweb:ws"); -const DEFAULT_CONNECT_DEBOUNCE = 1000; -const MAX_DEBOUNCE = 360000; +const REFRESH_THROTTLE_MS = 500; +const DEFAULT_CONNECT_DEBOUNCE_MS = 1000; +const MAX_CONNECT_DEBOUNCE_MS = 360000; + class WebsocketConnection { private wallets?: WalletMap; private ws?: WebSocketAsPromised; private reconnectionTimer?: number; private messageID = 1; - private connectDebounce = DEFAULT_CONNECT_DEBOUNCE; + private connectDebounce = DEFAULT_CONNECT_DEBOUNCE_MS; private forceClosing = false; + // TODO: automatically clean this up? + private refreshThrottles: Record void> = {}; + constructor(private dispatch: AppDispatch) { debug("WS component init"); this.attemptConnect(); @@ -62,7 +69,7 @@ this.messageID = 1; await this.ws.open(); - this.connectDebounce = DEFAULT_CONNECT_DEBOUNCE; + this.connectDebounce = DEFAULT_CONNECT_DEBOUNCE_MS; } async attemptConnect() { @@ -81,7 +88,7 @@ debug("failed to connect to server, reconnecting in %d ms", this.connectDebounce, err); this.reconnectionTimer = window.setTimeout(() => { - this.connectDebounce = Math.min(this.connectDebounce * 2, MAX_DEBOUNCE); + this.connectDebounce = Math.min(this.connectDebounce * 2, MAX_CONNECT_DEBOUNCE_MS); this.attemptConnect(); }, this.connectDebounce); } @@ -157,8 +164,26 @@ } /** Queues a command to re-fetch an address's balance. The response will be - * handled in {@link handleMessage}. */ + * handled in {@link handleMessage}. This is automatically throttled to + * execute on the leading edge of 500ms (REFRESH_THROTTLE_MS). */ refreshBalance(address: string) { + if (this.refreshThrottles[address]) { + // Use the existing throttled function if it exists + this.refreshThrottles[address](address); + } else { + // Create and cache a new throttle function for this address + const throttled = throttle( + this._refreshBalance.bind(this), + REFRESH_THROTTLE_MS, + { leading: true, trailing: false } + ); + + this.refreshThrottles[address] = throttled; + throttled(address); + } + } + + private _refreshBalance(address: string) { debug("refreshing balance of %s", address); this.ws?.sendPacked({ type: "address", id: this.messageID++, address }); }