Real-time metadata-sync & cell-locking¶
MetaVox ondersteunt real-time metadata-synchronisatie tussen gebruikers via Nextcloud's notify_push-app. Wanneer één gebruiker metadata bewerkt, zien alle andere gebruikers van dezelfde team folder de wijziging direct. Bovendien voorkomt cell-locking gelijktijdige edits op hetzelfde veld.

Vereisten¶
- Nextcloud notify_push-app geïnstalleerd en geconfigureerd
- Redis geconfigureerd als gedistribueerde cache in Nextcloud
- WebSocket reverse proxy geconfigureerd in Apache/Nginx
Installatie¶
# notify_push installeren
sudo -u www-data php /var/www/nextcloud/occ app:install notify_push
# Binary kopiëren
sudo cp /var/www/nextcloud/apps/notify_push/bin/x86_64/notify_push /usr/local/bin/notify_push
sudo chmod +x /usr/local/bin/notify_push
# systemd-service aanmaken
sudo tee /etc/systemd/system/notify_push.service << 'EOF'
[Unit]
Description=Nextcloud Push Notification Service
After=network.target
[Service]
Type=simple
User=www-data
ExecStart=/usr/local/bin/notify_push /var/www/nextcloud/config/config.php
Restart=always
RestartSec=5
Environment=PORT=7867
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now notify_push
# Apache reverse proxy
sudo tee /etc/apache2/conf-available/notify_push.conf << 'EOF'
ProxyPass /push/ws ws://127.0.0.1:7867/ws
ProxyPass /push/ http://127.0.0.1:7867/
ProxyPassReverse /push/ http://127.0.0.1:7867/
EOF
sudo a2enmod proxy proxy_http proxy_wstunnel
sudo a2enconf notify_push
sudo systemctl restart apache2
# Configureren
sudo -u www-data php /var/www/nextcloud/occ config:system:set trusted_proxies 0 --value='127.0.0.1'
sudo -u www-data php /var/www/nextcloud/occ config:app:set notify_push base_endpoint --value='https://your-domain.com/push'
# Verifiëren
sudo -u www-data php /var/www/nextcloud/occ notify_push:self-test
Alle checks moeten ✓ tonen.
Hoe het werkt¶
Real-time metadata-sync¶
Gebruiker A slaat metadata op
↓
FieldService::saveGroupfolderFileFieldValue()
↓
1. Database INSERT/UPDATE
2. Server-side cache geïnvalideerd
3. Push-event verzonden via Redis naar notify_push-daemon
↓
notify_push-daemon broadcast via WebSocket
↓
Browser van gebruiker B ontvangt push-event
↓
MetaVox haalt ALLEEN de metadata van het gewijzigde bestand opnieuw op (1 API-call)
↓
Cel werkt direct bij in grid-view van gebruiker B
Cell-locking¶

Gebruiker A dubbel-klikt een cel
↓
POST /api/groupfolders/{gfId}/files/{fileId}/lock
↓
Redis SET met 30s TTL: metavox_lock:{gfId}:{fileId}:{fieldName} = userId
↓
Push-event "metavox_cell_locked" naar alle groupfolder-leden
↓
Gebruiker B ziet:
- Oranje achtergrond-tint op de cel
- Oranje inset-border (2px)
- Cursor: not-allowed
- Hover-tooltip: "🔒 Wordt bewerkt door {username}"
- Dubbel-klik geblokkeerd
↓
Gebruiker A sluit de editor (opslaan of annuleren)
↓
POST /api/groupfolders/{gfId}/files/{fileId}/unlock
↓
Redis-key verwijderd + push-event "metavox_cell_unlocked"
↓
Cel van gebruiker B keert terug naar normaal
Crash-safety¶
Als gebruiker A het browsertabblad sluit of crasht zonder te ontgrendelen:
- De Redis lock-key heeft een 30-seconden TTL en verloopt automatisch
- Geen stale locks — de cel is na 30 seconden weer beschikbaar
- Geen handmatige interventie nodig
Technische details¶
Push-event-format¶
MetaVox gebruikt het notify_custom Redis-channel. De notify_push-daemon routet events naar specifieke gebruikers:
// PHP: verstuur naar elk groupfolder-lid
$this->notifyQueue->push('notify_custom', [
'user' => $userId,
'message' => 'metavox_metadata_changed',
'body' => [
'gfId' => $groupfolderId,
'fileId' => $fileId,
],
]);
Het WebSocket-bericht arriveert als: metavox_metadata_changed {"gfId":103,"fileId":456}
WebSocket-message-parsing¶
Nextcloud's ingebouwde push-client strip't de JSON-body bij dispatch naar _notify_push_listeners. MetaVox onderschept de raw WebSocket onmessage-handler om het volledige bericht inclusief body te parsen:
ws.onmessage = (event) => {
const raw = event.data // "metavox_metadata_changed {"gfId":103,"fileId":456}"
const spaceIdx = raw.indexOf(' ')
const eventName = raw.substring(0, spaceIdx) // "metavox_metadata_changed"
const body = JSON.parse(raw.substring(spaceIdx + 1)) // {gfId: 103, fileId: 456}
}
Lock-API¶
| Methode | Endpoint | Body | Response |
|---|---|---|---|
| POST | /api/groupfolders/{gfId}/files/{fileId}/lock |
{field_name: "doc_title"} |
{locked: false} (verkregen) of 409 {locked: true, lockedBy: "user"} |
| POST | /api/groupfolders/{gfId}/files/{fileId}/unlock |
{field_name: "doc_title"} |
{success: true} |
Lock-opslag¶
Locks worden opgeslagen in Redis via Nextcloud's gedistribueerde cache:
- Key:
metavox_lock:{groupfolderId}:{fileId}:{fieldName} - Value:
userId - TTL: 30 seconden (auto-verloopt)
Performance & schaalbaarheid¶
Push-events¶
| Metric | Waarde | Notities |
|---|---|---|
| Event-grootte | ~100 bytes | JSON met gfId + fileId |
| Redis PUBLISH-latency | ~0.1ms per gebruiker | Constant, ongeacht payload-grootte |
| WebSocket-delivery | ~10ms | Afhankelijk van netwerk-latency |
| 100 groupfolder-leden | ~10ms totaal | 100 Redis PUBLISH-calls |
| 2.000 groupfolder-leden | ~200ms totaal | 2000 Redis PUBLISH-calls |
| 10.000 groupfolder-leden | ~1s totaal | Binnen Redis-capaciteit |
Cell-locking¶
| Operatie | Backend-kosten | Netwerk-kosten |
|---|---|---|
| Lock verwerven | 1 Redis GET + 1 Redis SET | 1 POST-request |
| Lock-check (andere gebruiker probeert) | 1 Redis GET | 1 POST-request (409-response) |
| Unlock | 1 Redis GET + 1 Redis DELETE | 1 POST-request |
| Push-notificatie per lock/unlock | Zelfde als push-events | WebSocket (geen HTTP) |
Gelijktijdig-bewerken-scenario's¶
| Scenario | Gedrag |
|---|---|
| 2 gebruikers, zelfde cel | Tweede gebruiker ziet lock-indicator, kan niet bewerken |
| 2 gebruikers, verschillende cellen | Beide bewerken onafhankelijk, zien elkaars wijzigingen real-time |
| 50 gebruikers, zelfde folder | Allen zien metadata-wijzigingen direct, locks voorkomen conflicten |
| Gebruiker crasht tijdens edit | Lock verloopt na 30s, cel wordt beschikbaar |
| notify_push niet geïnstalleerd | Graceful fallback — geen real-time sync, handmatig vernieuwen nodig |
Redis-geheugen-gebruik¶
- Elke lock: ~100 bytes (key + value)
- 1.000 gelijktijdige edits: ~100 KB
- Locks verlopen na 30s — geen accumulatie
WebSocket-connecties¶
notify_push gebruikt één Rust-daemon die alle connecties afhandelt:
- Geheugen: ~5 KB per verbonden browser
- 10.000 verbonden browsers: ~50 MB RAM
- CPU: verwaarloosbaar (alleen event-routing)
Graceful degradation¶
Als notify_push niet is geïnstalleerd:
- Geen real-time sync — gebruikers moeten vernieuwen om elkaars wijzigingen te zien
- Geen cell-locking — de lock-API-call slaagt stilzwijgend (geen Redis = geen lock-opslag)
- Geen fouten — alle push-gerelateerde code handelt ontbrekend
notify_pushgraceful af - Volledige functionaliteit anders — inline-bewerken, weergaven, filters werken normaal
De FieldService-constructor wikkelt de IQueue-resolutie in een try-catch:
try {
$this->notifyQueue = \OC::$server->get(\OCA\NotifyPush\Queue\IQueue::class);
} catch (\Exception $e) {
// notify_push niet geïnstalleerd — real-time sync uitgeschakeld
}
Problemen oplossen¶
Push-events arriveren niet¶
- Check notify_push self-test:
occ notify_push:self-test— alle checks moeten ✓ zijn - Check WebSocket-connectie in browser:
console.log(window._notify_push_ws?.readyState)— moet1(OPEN) zijn - Check listener-registratie:
console.log(window._notify_push_ws?._metavoxPatched)— moettruezijn - Check daemon-logs:
journalctl -u notify_push -f
Lock-indicator wordt niet getoond¶
- Verifieer dat het push-event arriveert: onderschep WebSocket met
ws.onmessage-logging - Check of cel-selector matches:
document.querySelector('.metavox-col[data-file-id="123"]') - Zorg dat twee verschillende gebruikers testen (eigen locks worden verborgen)
Stale data na edit¶
- Server-side per-file-cache is verwijderd — alle metadata-reads gaan direct naar de database
- Als data stale lijkt, check Redis-cache-TTL op de filter-values-cache (300s)