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
$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);

View File

@@ -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<Stack> */
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;
}
}

View File

@@ -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;
}
}

View File

@@ -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]);

View File

@@ -26,4 +26,5 @@ namespace OCA\Deck;
class NotifyPushEvents {
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->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);
}

View File

@@ -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?

View File

@@ -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 }) {