From 322ee9257311e01e6638ec40d9d08ad663ee5577 Mon Sep 17 00:00:00 2001 From: chandi Langecker Date: Mon, 21 Nov 2022 20:10:27 +0100 Subject: [PATCH] live updates for boards Signed-off-by: chandi Langecker --- lib/AppInfo/Application.php | 6 +++++ lib/Db/StackMapper.php | 21 ++++++++++++++--- lib/Event/CardUpdatedEvent.php | 12 ++++++++++ lib/Listeners/LiveUpdateListener.php | 34 ++++++++++++++++++++++++---- lib/NotifyPushEvents.php | 1 + lib/Service/CardService.php | 4 +++- src/sessions.js | 12 ++++++++++ src/store/main.js | 7 +++++- 8 files changed, 88 insertions(+), 9 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 1363cb299..2579df32e 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -154,6 +154,12 @@ class Application extends App implements IBootstrap { // Event listening for realtime updates via notify_push $context->registerEventListener(SessionCreatedEvent::class, LiveUpdateListener::class); $context->registerEventListener(SessionClosedEvent::class, LiveUpdateListener::class); + $context->registerEventListener(CardCreatedEvent::class, LiveUpdateListener::class); + $context->registerEventListener(CardUpdatedEvent::class, LiveUpdateListener::class); + $context->registerEventListener(CardDeletedEvent::class, LiveUpdateListener::class); + $context->registerEventListener(AclCreatedEvent::class, LiveUpdateListener::class); + $context->registerEventListener(AclUpdatedEvent::class, LiveUpdateListener::class); + $context->registerEventListener(AclDeletedEvent::class, LiveUpdateListener::class); $context->registerNotifierService(Notifier::class); $context->registerEventListener(LoadAdditionalScriptsEvent::class, ResourceAdditionalScriptsListener::class); diff --git a/lib/Db/StackMapper.php b/lib/Db/StackMapper.php index 624c739e7..162f1ee56 100644 --- a/lib/Db/StackMapper.php +++ b/lib/Db/StackMapper.php @@ -29,16 +29,24 @@ use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\Cache\CappedMemoryCache; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use OCP\ICache; +use OCP\ICacheFactory; /** @template-extends DeckMapper */ class StackMapper extends DeckMapper implements IPermissionMapper { private CappedMemoryCache $stackCache; private CardMapper $cardMapper; + private ICache $cache; - public function __construct(IDBConnection $db, CardMapper $cardMapper) { + public function __construct( + IDBConnection $db, + CardMapper $cardMapper, + ICacheFactory $cacheFactory + ) { parent::__construct($db, 'deck_stacks', Stack::class); $this->cardMapper = $cardMapper; $this->stackCache = new CappedMemoryCache(); + $this->cache = $cacheFactory->createDistributed('deck-stackMapper'); } @@ -157,12 +165,19 @@ class StackMapper extends DeckMapper implements IPermissionMapper { * @throws \OCP\DB\Exception */ public function findBoardId($id): ?int { + $result = $this->cache->get('findBoardId:' . $id); + if ($result !== null) { + return $result !== false ? $result : null; + } try { $entity = $this->find($id); - return $entity->getBoardId(); + $result = $entity->getBoardId(); } catch (DoesNotExistException $e) { + $result = false; } catch (MultipleObjectsReturnedException $e) { } - return null; + $this->cache->set('findBoardId:' . $id, $result); + + return $result !== false ? $result : null; } } diff --git a/lib/Event/CardUpdatedEvent.php b/lib/Event/CardUpdatedEvent.php index 5e991691f..e5006f8df 100644 --- a/lib/Event/CardUpdatedEvent.php +++ b/lib/Event/CardUpdatedEvent.php @@ -26,5 +26,17 @@ declare(strict_types=1); namespace OCA\Deck\Event; +use OCA\Deck\Db\Card; + class CardUpdatedEvent extends ACardEvent { + private $cardBefore; + + public function __construct(Card $card, Card $before = null) { + parent::__construct($card); + $this->cardBefore = $before; + } + + public function getCardBefore() { + return $this->cardBefore; + } } diff --git a/lib/Listeners/LiveUpdateListener.php b/lib/Listeners/LiveUpdateListener.php index 8138488f2..2a4875566 100644 --- a/lib/Listeners/LiveUpdateListener.php +++ b/lib/Listeners/LiveUpdateListener.php @@ -26,7 +26,11 @@ declare(strict_types=1); namespace OCA\Deck\Listeners; +use OCA\Deck\Db\StackMapper; use OCA\Deck\NotifyPushEvents; +use OCA\Deck\Event\AAclEvent; +use OCA\Deck\Event\ACardEvent; +use OCA\Deck\Event\CardUpdatedEvent; use OCA\Deck\Event\SessionClosedEvent; use OCA\Deck\Event\SessionCreatedEvent; use OCA\Deck\Service\SessionService; @@ -42,13 +46,15 @@ class LiveUpdateListener implements IEventListener { private LoggerInterface $logger; private SessionService $sessionService; private IRequest $request; + private StackMapper $stackMapper; private $queue; public function __construct( ContainerInterface $container, IRequest $request, LoggerInterface $logger, - SessionService $sessionService + SessionService $sessionService, + StackMapper $stackMapper ) { try { $this->queue = $container->get(IQueue::class); @@ -59,6 +65,7 @@ class LiveUpdateListener implements IEventListener { $this->logger = $logger; $this->sessionService = $sessionService; $this->request = $request; + $this->stackMapper = $stackMapper; } public function handle(Event $event): void { @@ -68,17 +75,36 @@ class LiveUpdateListener implements IEventListener { } try { - // the web frontend is adding the Session-ID as a header on every request + // the web frontend is adding the Session-ID as a header // TODO: verify the token! this currently allows to spoof a token from someone - // else, preventing this person from getting any live updates + // else, preventing this person from getting updates $causingSessionToken = $this->request->getHeader('x-nc-deck-session'); if ( $event instanceof SessionCreatedEvent || - $event instanceof SessionClosedEvent + $event instanceof SessionClosedEvent || + $event instanceof AAclEvent ) { $this->sessionService->notifyAllSessions($this->queue, $event->getBoardId(), NotifyPushEvents::DeckBoardUpdate, [ 'id' => $event->getBoardId() ], $causingSessionToken); + } elseif ($event instanceof ACardEvent) { + $boardId = $this->stackMapper->findBoardId($event->getCard()->getStackId()); + $this->sessionService->notifyAllSessions($this->queue, $boardId, NotifyPushEvents::DeckCardUpdate, [ + 'boardId' => $boardId, + 'cardId' => $event->getCard()->getId() + ], $causingSessionToken); + + // if card got moved to a diferent board, we should notify + // also sessions active on the previous board + if ($event instanceof CardUpdatedEvent && $event->getCardBefore()) { + $previousBoardId = $this->stackMapper->findBoardId($event->getCardBefore()->getStackId()); + if ($boardId !== $previousBoardId) { + $this->sessionService->notifyAllSessions($this->queue, $previousBoardId, NotifyPushEvents::DeckCardUpdate, [ + 'boardId' => $boardId, + 'cardId' => $event->getCard()->getId() + ], $causingSessionToken); + } + } } } catch (\Exception $e) { $this->logger->error('Error when handling live update event', ['exception' => $e]); diff --git a/lib/NotifyPushEvents.php b/lib/NotifyPushEvents.php index f12daa582..e275ea554 100644 --- a/lib/NotifyPushEvents.php +++ b/lib/NotifyPushEvents.php @@ -26,4 +26,5 @@ namespace OCA\Deck; class NotifyPushEvents { public const DeckBoardUpdate = 'deck_board_update'; + public const DeckCardUpdate = 'deck_card_update'; } diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index b44a094d9..374111cb9 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -353,7 +353,7 @@ class CardService { } $this->changeHelper->cardChanged($card->getId(), true); - $this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card)); + $this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card, $changes->getBefore())); return $card; } @@ -443,6 +443,8 @@ class CardService { $result[$card->getOrder()] = $card; } $this->changeHelper->cardChanged($id, false); + $this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card)); + return array_values($result); } diff --git a/src/sessions.js b/src/sessions.js index 942d34084..52cbf948d 100644 --- a/src/sessions.js +++ b/src/sessions.js @@ -52,6 +52,18 @@ hasPush = listen('deck_board_update', (name, body) => { store.dispatch('refreshBoard', currentBoardId) }) +listen('deck_card_update', (name, body) => { + + // ignore update events which we have triggered ourselves + if (isOurSessionToken(body._causingSessionToken)) return + + // only handle update events for the currently open board + const currentBoardId = store.state.currentBoard?.id + if (body.boardId !== currentBoardId) return + + store.dispatch('loadStacks', currentBoardId) +}) + /** * is the notify_push app active and can * provide us with real time updates? diff --git a/src/store/main.js b/src/store/main.js index 7c798dbf6..31b52c84c 100644 --- a/src/store/main.js +++ b/src/store/main.js @@ -333,10 +333,15 @@ export default new Vuex.Store({ commit('setAssignableUsers', board.users) }, - async refreshBoard({ commit }, boardId) { + async refreshBoard({ commit, dispatch }, boardId) { const board = await apiClient.loadById(boardId) + const etagHasChanged = board.ETag !== this.state.currentBoard.ETag commit('setCurrentBoard', board) commit('setAssignableUsers', board.users) + + if (etagHasChanged) { + dispatch('loadStacks', boardId) + } }, toggleShowArchived({ commit }) {