live updates for boards

Signed-off-by: chandi Langecker <git@chandi.it>
This commit is contained in:
chandi Langecker
2022-11-21 20:10:27 +01:00
committed by Julius Härtl
parent 9674c344ea
commit 322ee92573
8 changed files with 88 additions and 9 deletions

View File

@@ -154,6 +154,12 @@ class Application extends App implements IBootstrap {
// Event listening for realtime updates via notify_push // Event listening for realtime updates via notify_push
$context->registerEventListener(SessionCreatedEvent::class, LiveUpdateListener::class); $context->registerEventListener(SessionCreatedEvent::class, LiveUpdateListener::class);
$context->registerEventListener(SessionClosedEvent::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->registerNotifierService(Notifier::class);
$context->registerEventListener(LoadAdditionalScriptsEvent::class, ResourceAdditionalScriptsListener::class); $context->registerEventListener(LoadAdditionalScriptsEvent::class, ResourceAdditionalScriptsListener::class);

View File

@@ -29,16 +29,24 @@ use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\Cache\CappedMemoryCache; use OCP\Cache\CappedMemoryCache;
use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\ICache;
use OCP\ICacheFactory;
/** @template-extends DeckMapper<Stack> */ /** @template-extends DeckMapper<Stack> */
class StackMapper extends DeckMapper implements IPermissionMapper { class StackMapper extends DeckMapper implements IPermissionMapper {
private CappedMemoryCache $stackCache; private CappedMemoryCache $stackCache;
private CardMapper $cardMapper; 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); parent::__construct($db, 'deck_stacks', Stack::class);
$this->cardMapper = $cardMapper; $this->cardMapper = $cardMapper;
$this->stackCache = new CappedMemoryCache(); $this->stackCache = new CappedMemoryCache();
$this->cache = $cacheFactory->createDistributed('deck-stackMapper');
} }
@@ -157,12 +165,19 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
* @throws \OCP\DB\Exception * @throws \OCP\DB\Exception
*/ */
public function findBoardId($id): ?int { public function findBoardId($id): ?int {
$result = $this->cache->get('findBoardId:' . $id);
if ($result !== null) {
return $result !== false ? $result : null;
}
try { try {
$entity = $this->find($id); $entity = $this->find($id);
return $entity->getBoardId(); $result = $entity->getBoardId();
} catch (DoesNotExistException $e) { } catch (DoesNotExistException $e) {
$result = false;
} catch (MultipleObjectsReturnedException $e) { } catch (MultipleObjectsReturnedException $e) {
} }
return null; $this->cache->set('findBoardId:' . $id, $result);
return $result !== false ? $result : null;
} }
} }

View File

@@ -26,5 +26,17 @@ declare(strict_types=1);
namespace OCA\Deck\Event; namespace OCA\Deck\Event;
use OCA\Deck\Db\Card;
class CardUpdatedEvent extends ACardEvent { 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;
}
} }

View File

@@ -26,7 +26,11 @@ declare(strict_types=1);
namespace OCA\Deck\Listeners; namespace OCA\Deck\Listeners;
use OCA\Deck\Db\StackMapper;
use OCA\Deck\NotifyPushEvents; 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\SessionClosedEvent;
use OCA\Deck\Event\SessionCreatedEvent; use OCA\Deck\Event\SessionCreatedEvent;
use OCA\Deck\Service\SessionService; use OCA\Deck\Service\SessionService;
@@ -42,13 +46,15 @@ class LiveUpdateListener implements IEventListener {
private LoggerInterface $logger; private LoggerInterface $logger;
private SessionService $sessionService; private SessionService $sessionService;
private IRequest $request; private IRequest $request;
private StackMapper $stackMapper;
private $queue; private $queue;
public function __construct( public function __construct(
ContainerInterface $container, ContainerInterface $container,
IRequest $request, IRequest $request,
LoggerInterface $logger, LoggerInterface $logger,
SessionService $sessionService SessionService $sessionService,
StackMapper $stackMapper
) { ) {
try { try {
$this->queue = $container->get(IQueue::class); $this->queue = $container->get(IQueue::class);
@@ -59,6 +65,7 @@ class LiveUpdateListener implements IEventListener {
$this->logger = $logger; $this->logger = $logger;
$this->sessionService = $sessionService; $this->sessionService = $sessionService;
$this->request = $request; $this->request = $request;
$this->stackMapper = $stackMapper;
} }
public function handle(Event $event): void { public function handle(Event $event): void {
@@ -68,17 +75,36 @@ class LiveUpdateListener implements IEventListener {
} }
try { 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 // 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'); $causingSessionToken = $this->request->getHeader('x-nc-deck-session');
if ( if (
$event instanceof SessionCreatedEvent || $event instanceof SessionCreatedEvent ||
$event instanceof SessionClosedEvent $event instanceof SessionClosedEvent ||
$event instanceof AAclEvent
) { ) {
$this->sessionService->notifyAllSessions($this->queue, $event->getBoardId(), NotifyPushEvents::DeckBoardUpdate, [ $this->sessionService->notifyAllSessions($this->queue, $event->getBoardId(), NotifyPushEvents::DeckBoardUpdate, [
'id' => $event->getBoardId() 'id' => $event->getBoardId()
], $causingSessionToken); ], $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) { } catch (\Exception $e) {
$this->logger->error('Error when handling live update event', ['exception' => $e]); $this->logger->error('Error when handling live update event', ['exception' => $e]);

View File

@@ -26,4 +26,5 @@ namespace OCA\Deck;
class NotifyPushEvents { class NotifyPushEvents {
public const DeckBoardUpdate = 'deck_board_update'; public const DeckBoardUpdate = 'deck_board_update';
public const DeckCardUpdate = 'deck_card_update';
} }

View File

@@ -353,7 +353,7 @@ class CardService {
} }
$this->changeHelper->cardChanged($card->getId(), true); $this->changeHelper->cardChanged($card->getId(), true);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card)); $this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card, $changes->getBefore()));
return $card; return $card;
} }
@@ -443,6 +443,8 @@ class CardService {
$result[$card->getOrder()] = $card; $result[$card->getOrder()] = $card;
} }
$this->changeHelper->cardChanged($id, false); $this->changeHelper->cardChanged($id, false);
$this->eventDispatcher->dispatchTyped(new CardUpdatedEvent($card));
return array_values($result); return array_values($result);
} }

View File

@@ -52,6 +52,18 @@ hasPush = listen('deck_board_update', (name, body) => {
store.dispatch('refreshBoard', currentBoardId) 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 * is the notify_push app active and can
* provide us with real time updates? * provide us with real time updates?

View File

@@ -333,10 +333,15 @@ export default new Vuex.Store({
commit('setAssignableUsers', board.users) commit('setAssignableUsers', board.users)
}, },
async refreshBoard({ commit }, boardId) { async refreshBoard({ commit, dispatch }, boardId) {
const board = await apiClient.loadById(boardId) const board = await apiClient.loadById(boardId)
const etagHasChanged = board.ETag !== this.state.currentBoard.ETag
commit('setCurrentBoard', board) commit('setCurrentBoard', board)
commit('setAssignableUsers', board.users) commit('setAssignableUsers', board.users)
if (etagHasChanged) {
dispatch('loadStacks', boardId)
}
}, },
toggleShowArchived({ commit }) { toggleShowArchived({ commit }) {