From f1623c1286ca785b8f2a9f13ce49a0ee39d2dfa5 Mon Sep 17 00:00:00 2001 From: Stamates Date: Thu, 27 Sep 2018 13:36:07 -0400 Subject: [PATCH] Use place holders when receiving out of order blocks realtime --- .../assets/__tests__/pages/block.js | 60 ++++++++++++++-- .../assets/__tests__/pages/transaction.js | 11 +++ apps/block_scout_web/assets/js/pages/block.js | 69 +++++++++++++++++-- .../templates/block/_tile.html.eex | 2 +- .../features/pages/block_list_page.ex | 8 ++- .../features/viewing_blocks_test.exs | 31 ++++++++- 6 files changed, 164 insertions(+), 17 deletions(-) diff --git a/apps/block_scout_web/assets/__tests__/pages/block.js b/apps/block_scout_web/assets/__tests__/pages/block.js index 0f50e9cf7f..6f06a31a21 100644 --- a/apps/block_scout_web/assets/__tests__/pages/block.js +++ b/apps/block_scout_web/assets/__tests__/pages/block.js @@ -1,13 +1,59 @@ import { reducer, initialState } from '../../js/pages/block' -test('RECEIVED_NEW_BLOCK', () => { + +test('CHANNEL_DISCONNECTED', () => { + const state = initialState const action = { - type: 'RECEIVED_NEW_BLOCK', - msg: { - blockHtml: "test" - } + type: 'CHANNEL_DISCONNECTED' } - const output = reducer(initialState, action) + const output = reducer(state, action) + + expect(output.channelDisconnected).toBe(true) +}) + +describe('RECEIVED_NEW_BLOCK', () => { + test('receives new block', () => { + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + blockHtml: 'test', + blockNumber: 1 + } + } + const output = reducer(initialState, action) + + expect(output.newBlock).toBe('test') + expect(output.currentBlockNumber).toBe(1) + }) + test('on page 2+', () => { + const state = Object.assign({}, initialState, { + beyondPageOne: true + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msgs: [{ + blockHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.newBlock).toBe(null) + }) + test('inserts place holders if block received out of order', () => { + const state = Object.assign({}, initialState, { + currentBlockNumber: 2 + }) + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + blockHtml: 'test5', + blockNumber: 5 + } + } + const output = reducer(state, action) - expect(output.newBlock).toBe("test") + expect(output.newBlock).toBe('test5') + expect(output.currentBlockNumber).toBe(5) + expect(output.skippedBlockNumbers).toEqual([3, 4]) + }) }) diff --git a/apps/block_scout_web/assets/__tests__/pages/transaction.js b/apps/block_scout_web/assets/__tests__/pages/transaction.js index b4ba0f62b7..ea4eb3595f 100644 --- a/apps/block_scout_web/assets/__tests__/pages/transaction.js +++ b/apps/block_scout_web/assets/__tests__/pages/transaction.js @@ -1,5 +1,16 @@ import { reducer, initialState } from '../../js/pages/transaction' +test('CHANNEL_DISCONNECTED', () => { + const state = initialState + const action = { + type: 'CHANNEL_DISCONNECTED' + } + const output = reducer(state, action) + + expect(output.channelDisconnected).toBe(true) + expect(output.batchCountAccumulator).toBe(0) +}) + test('RECEIVED_NEW_BLOCK', () => { const state = { ...initialState, blockNumber: 1 } const action = { diff --git a/apps/block_scout_web/assets/js/pages/block.js b/apps/block_scout_web/assets/js/pages/block.js index 17cf0eec5a..3238d01cac 100644 --- a/apps/block_scout_web/assets/js/pages/block.js +++ b/apps/block_scout_web/assets/js/pages/block.js @@ -1,4 +1,5 @@ import $ from 'jquery' +import _ from 'lodash' import URI from 'urijs' import humps from 'humps' import socket from '../socket' @@ -8,14 +9,22 @@ import { initRedux, prependWithClingBottom } from '../utils' export const initialState = { beyondPageOne: null, channelDisconnected: false, - newBlock: null + currentBlockNumber: null, + newBlock: null, + replaceBlock: null, + skippedBlockNumbers: [] } export function reducer (state = initialState, action) { switch (action.type) { case 'PAGE_LOAD': { + let blockNumber = parseInt(state.currentBlockNumber) + if (!action.beyondPageOne) { + blockNumber = parseInt(action.highestBlockNumber) + } return Object.assign({}, state, { - beyondPageOne: action.beyondPageOne + beyondPageOne: action.beyondPageOne, + currentBlockNumber: blockNumber }) } case 'CHANNEL_DISCONNECTED': { @@ -24,10 +33,24 @@ export function reducer (state = initialState, action) { }) } case 'RECEIVED_NEW_BLOCK': { - if (state.channelDisconnected) return state + if (state.channelDisconnected || state.beyondPageOne) return state + let skippedBlockNumbers = state.skippedBlockNumbers.slice(0) + let replaceBlock = null + const blockNumber = parseInt(action.msg.blockNumber) + if (blockNumber > state.currentBlockNumber + 1) { + for (let i = state.currentBlockNumber + 1; i < action.msg.blockNumber; i++) { + skippedBlockNumbers.push(i) + } + } else if (_.indexOf(skippedBlockNumbers, blockNumber) != -1) { + skippedBlockNumbers = _.without(skippedBlockNumbers, blockNumber) + replaceBlock = blockNumber + } return Object.assign({}, state, { - newBlock: action.msg.blockHtml + currentBlockNumber: blockNumber > state.currentBlockNumber ? blockNumber : state.currentBlockNumber, + newBlock: action.msg.blockHtml, + replaceBlock, + skippedBlockNumbers }) } default: @@ -41,7 +64,8 @@ if ($blockListPage.length) { main (store) { const state = store.dispatch({ type: 'PAGE_LOAD', - beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).blockNumber + beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).blockNumber, + highestBlockNumber: $('[data-selector="block-number"]').filter(':first').text() }) if (!state.beyondPageOne) { const blocksChannel = socket.channel(`blocks:new_block`, {}) @@ -58,9 +82,42 @@ if ($blockListPage.length) { if (state.channelDisconnected) $channelDisconnected.show() if (oldState.newBlock !== state.newBlock) { - prependWithClingBottom($blocksList, state.newBlock) + if (oldState.skippedBlockNumbers !== state.skippedBlockNumbers) { + const newSkippedBlockNumbers = _.difference(state.skippedBlockNumbers, oldState.skippedBlockNumbers) + if (state.replaceBlock) { + const $placeHolder = $(`[data-selector="place-holder"][data-block-number="${state.replaceBlock}"]`) + $placeHolder.addClass('shrink-out') + setTimeout(() => $placeHolder.slideUp({ + complete: () => { + $placeHolder.replaceWith(state.newBlock) + } + }), 400) + } else { + _.map(newSkippedBlockNumbers, (skippedBlockNumber) => { + prependWithClingBottom($blocksList, placeHolderBlock(skippedBlockNumber)) + }) + prependWithClingBottom($blocksList, state.newBlock) + } + } else { + prependWithClingBottom($blocksList, state.newBlock) + } updateAllAges() } } }) } + +function placeHolderBlock(blockNumber) { + return ` +
+
+
+ ${blockNumber} +
+
+ Block Mined, awaiting import... +
+
+
+ ` +} diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex index 302490c464..5a457b7c43 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex @@ -6,7 +6,7 @@ @block, class: "tile-title", to: block_path(BlockScoutWeb.Endpoint, :show, @block), - "data-test": "block_number", + "data-selector": "block-number", "data-block-number": to_string(@block.number) ) %>
diff --git a/apps/block_scout_web/test/block_scout_web/features/pages/block_list_page.ex b/apps/block_scout_web/test/block_scout_web/features/pages/block_list_page.ex index 6b87654b3e..a550c000d2 100644 --- a/apps/block_scout_web/test/block_scout_web/features/pages/block_list_page.ex +++ b/apps/block_scout_web/test/block_scout_web/features/pages/block_list_page.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.BlockListPage do use Wallaby.DSL - import Wallaby.Query, only: [css: 1] + import Wallaby.Query, only: [css: 1, css: 2] alias Explorer.Chain.Block @@ -12,6 +12,10 @@ defmodule BlockScoutWeb.BlockListPage do end def block(%Block{number: block_number}) do - css("[data-test='block_number'][data-block-number='#{block_number}']") + css("[data-selector='block-number'][data-block-number='#{block_number}']") + end + + def place_holder_blocks(count) do + css("[data-selector='place-holder']", count: count) end end diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_blocks_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_blocks_test.exs index 274a79298a..34ff36128e 100644 --- a/apps/block_scout_web/test/block_scout_web/features/viewing_blocks_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/viewing_blocks_test.exs @@ -1,7 +1,7 @@ defmodule BlockScoutWeb.ViewingBlocksTest do use BlockScoutWeb.FeatureCase, async: true - alias BlockScoutWeb.{BlockListPage, BlockPage} + alias BlockScoutWeb.{BlockListPage, BlockPage, Notifier} setup do timestamp = Timex.now() |> Timex.shift(hours: -1) @@ -35,6 +35,35 @@ defmodule BlockScoutWeb.ViewingBlocksTest do |> assert_has(BlockPage.detail_number(block)) end + test "inserts place holder blocks if out of order block received", %{session: session} do + BlockListPage.visit_page(session) + + block = insert(:block, number: 315) + Notifier.handle_event({:chain_event, :blocks, [block]}) + + session + |> assert_has(BlockListPage.block(block)) + |> assert_has(BlockListPage.place_holder_blocks(3)) + end + + test "replaces place holder block if skipped block received", %{session: session} do + BlockListPage.visit_page(session) + + block = insert(:block, number: 315) + Notifier.handle_event({:chain_event, :blocks, [block]}) + + session + |> assert_has(BlockListPage.block(block)) + |> assert_has(BlockListPage.place_holder_blocks(3)) + + skipped_block = insert(:block, number: 314) + Notifier.handle_event({:chain_event, :blocks, [skipped_block]}) + + session + |> assert_has(BlockListPage.block(skipped_block)) + |> assert_has(BlockListPage.place_holder_blocks(2)) + end + test "block detail page has transactions", %{session: session} do block = insert(:block, number: 42)