Documentation Frappe Framework

Frappe Framework

Temps réel (socket.io)

Frappe fournit une API d’événements temps réel fondée sur socket.io : écoute côté navigateur, publication côté serveur, gestionnaires personnalisés et salles d’abonnement.

8 min de lectureMis à jour le Source : docs.frappe.io

À quoi sert l’API temps réel

Frappe embarque une API d’événements temps réel (Realtime) fondée sur socket.io. Comme socket.io a besoin d’un serveur Node pour fonctionner, un processus Node tourne en parallèle du serveur web principal.

API client (JavaScript)

frappe.realtime.on

Pour écouter un événement temps réel côté client (navigateur), utilisez la méthode frappe.realtime.on :

js
frappe.realtime.on('event_name', (data) => {
    console.log(data)
})

frappe.realtime.off

Pour cesser d’écouter un événement auquel vous vous étiez abonné :

js
frappe.realtime.off('event_name')

API serveur (Python)

frappe.publish_realtime

Pour publier un événement temps réel depuis le serveur, utilisez la méthode frappe.publish_realtime :

python
frappe.publish_realtime('event_name', data={'key': 'value'})

frappe.publish_progress

Cette méthode affiche une barre de progression dans une boîte de dialogue :

python
frappe.publish_progress(25, title='Some title', description='Some description')

La documentation officielle illustre le résultat par une capture d’écran de la barre de progression.

Gestionnaires d’événements personnalisés (Python)

À retenir

Cette fonctionnalité n’est disponible que dans la version nightly et elle est considérée comme expérimentale.

Vous pouvez implémenter vos propres gestionnaires d’événements temps réel en créant un fichier your_app/your_app/realtime/handlers.py dans votre app. La syntaxe ressemble beaucoup à celle des méthodes d’API autorisées (whitelist).

python
from frappe.realtime import realtime

@realtime.on(
  "project_subscribe",
  frappe_context=False,   # open a Frappe context (DB + session) for the handler body
  allow_guest=False,      # if False, the event is dropped when socket.user == "Guest"
)
ParamètreValeur par défautRôle
frappe_contextFalseOuvre un contexte Frappe complet (base de données + session) pour le corps du gestionnaire.
allow_guestFalseSi False, l’événement est ignoré quand socket.user == "Guest".

Exemple

python
import frappe
from frappe.realtime import Socket, realtime

@realtime.on("project_subscribe")
def project_subscribe(socket: Socket, project: str) -> None:
    if socket.has_permission("Project", project):
        socket.join(f"project:{project}")

Le premier argument est toujours l’objet Socket typé. Les arguments suivants sont la charge utile envoyée par le client, dans l’ordre. Le nom d’événement "project_subscribe" est celui que le navigateur émet avec frappe.realtime.emit(...).

Exemple fonctionnel avec frappe_context=True

Avec frappe_context=True, l’ORM complet est disponible dans le corps du gestionnaire : frappe.get_doc, frappe.local.site et frappe.session.user sont renseignés et correspondent à l’utilisateur authentifié sur le socket.

Ce gestionnaire charge un document, vérifie la permission puis renvoie le résultat au client :

python
@realtime.on("test_get_doc", frappe_context=True)
def test_get_doc(socket: Socket, doctype: str, docname: str) -> None:
    import frappe
    try:
        doc = frappe.get_doc(doctype, docname)
        doc.check_permission()
        socket.emit(
            "test_get_doc_result",
            {
                "ok": True,
                "site": frappe.local.site,
                "user": frappe.session.user,
                "doctype": doc.doctype,
                "name": doc.name,
                "modified": str(doc.get("modified")),
            },
        )
    except Exception as e:
        socket.emit("test_get_doc_result", {"ok": False, "error": f"{type(e).__name__}: {e}"})

Depuis le navigateur :

js
frappe.realtime.on("test_get_doc_result", (r) => console.log(r));
frappe.realtime.emit("test_get_doc", "User", "Administrator");

Voir la section « Vérifications de permissions » ci-dessous pour le coût de cette approche et les cas où il vaut mieux préférer la vérification HTTP, moins coûteuse.

Un gestionnaire ne s’exécute que si l’app qui le possède est installée sur le site qui se connecte. L’app propriétaire est détectée automatiquement à partir de l’import.

Il peut être nécessaire de redémarrer le serveur socketio pour que les modifications de code soient prises en compte. Consultez la documentation Socket.IO pour en savoir plus sur l’écriture de gestionnaires d’événements.

Objet Socket

Identité en lecture seule, renseignée à la connexion :

python
socket.site            # the site this socket is on
socket.user            # "Guest" for anonymous
socket.user_type       # e.g. "System User"
socket.installed_apps  # list of installed apps

Salles (rooms), émission et état transitoire :

python
socket.join(room)                        # add this socket to a room
socket.leave(room)                       # remove it
socket.emit(event, data=None, room=None) # emit to a room, or to this client if room is None
socket.get(key, default=None)            # read transient per-socket state
socket.set(key, value)                   # persist transient per-socket state (cleared on disconnect)
Méthode / attributRôle
socket.siteLe site auquel ce socket est rattaché.
socket.userL’utilisateur connecté ; "Guest" pour un anonyme.
socket.user_typeLe type d’utilisateur, p. ex. "System User".
socket.installed_appsLa liste des apps installées.
socket.join(room)Ajoute ce socket à une salle.
socket.leave(room)Retire ce socket de la salle.
socket.emit(event, data=None, room=None)Émet vers une salle, ou vers ce client seul si room vaut None.
socket.get(key, default=None)Lit un état transitoire propre au socket.
socket.set(key, value)Enregistre un état transitoire propre au socket (effacé à la déconnexion).

Vérifications de permissions

Il existe deux façons de vérifier les permissions sur un DocType.

A. HTTP (par défaut, recommandé) : socket.has_permission(doctype, name) interroge le processus web, exactement comme les gestionnaires du cœur. Aucune connexion à la base de données n’est ouverte dans le processus temps réel. C’est peu coûteux : utilisez cette méthode sauf raison particulière.

python
if socket.has_permission("Project", project):
    socket.join(f"project:{project}")

B. Dans le processus (frappe_context=True) : ouvre un contexte Frappe complet pour le corps du gestionnaire, ce qui permet d’appeler directement frappe.has_permission(...), d’interroger la base de données, etc. Coût : chaque événement de ce type paie la séquence frappe.init -> connect -> set_user -> commit/rollback -> destroy et impose une connexion à la base de données dans le processus temps réel. À utiliser avec parcimonie.

python
@realtime.on("project_subscribe", frappe_context=True)
def project_subscribe(socket: Socket, project: str) -> None:
    if frappe.has_permission("Project", doc=project, ptype="read"):
        socket.join(f"project:{project}")

Référence complète : frappe/realtime/README.md

Gestionnaires d’événements personnalisés (NodeJS)

Attention

Disponible à partir de la v16 et considéré comme expérimental. Il s’agit de l’implémentation précédente, qui sera progressivement abandonnée dans les versions futures. Les nouvelles apps doivent préférer les gestionnaires Python décrits ci-dessus.

Vous pouvez implémenter des gestionnaires d’événements temps réel côté serveur Socket.IO en créant un fichier handlers.js dans le dossier realtime de votre app.

Ce fichier doit avoir un seul export : une fonction qui installe les gestionnaires d’événements sur l’instance de socket. Par exemple, pour une app nommée « chat » :

js
// bench/apps/chat/realtime/handlers.js

function chat_app_handlers(socket) {
    socket.on("hello_chat", () => {
        console.log("hello world!");
    });
}

module.exports = chat_app_handlers;

Déclenchez cet événement depuis le code client avec frappe.realtime.emit("hello_chat"). Il peut être nécessaire de redémarrer le serveur socketio pour que les modifications soient prises en compte. Consultez la documentation Socket.IO pour en savoir plus.

Pousser des événements vers les clients

Le processus web n’émet pas directement : il publie via Redis, et le serveur temps réel fait le pont vers les sockets connectés. La chaîne de salle vers laquelle vous publiez doit correspondre à la salle que votre gestionnaire a rejointe avec join.

python
from frappe.realtime import publish_to_room
publish_to_room("project:PROJ-0001", "project_updated", {"status": "Open"})

D’autres fonctions nommées existent ; chacune est une fine surcouche de publish_realtime :

python
publish_to_user(user, event, message=None)
publish_to_doc(doctype, docname, event, message=None)
publish_to_doctype(doctype, event, message=None)
publish_task_progress(task_id, message=None)
publish_to_website(event, message=None)
publish_to_all(event, message=None)
publish_to_room(room, event, message=None)

Référence complète : frappe/realtime/README.md

Client personnalisé

Vous pouvez écrire un client personnalisé qui se connecte au serveur socket.io si vous développez une SPA ou une application mobile qui n’utilise pas l’interface du bureau (Desk). Reportez-vous à la documentation officielle du client Socket.IO.

Exemples de clients personnalisés :

Autorisation dans un client personnalisé

Deux méthodes permettent d’authentifier une connexion auprès du serveur socket.io :

  • Cookies : dans un environnement de type navigateur, la connexion envoie automatiquement les cookies et le serveur socketio s’en sert pour authentifier.
  • En-tête Authorization : si les cookies ne sont pas disponibles (applications mobiles, par exemple), utilisez un en-tête Authorization comme pour les requêtes d’API. Voir l’authentification de l’API REST et l’option extraHeaders de Socket.IO.

Notes d’implémentation

  • Le serveur temps réel repose sur le serveur socket.io, écrit en Node.js, situé dans le dossier /realtime.
  • Le client temps réel est une surcouche de la bibliothèque cliente socket.io, située dans public/js/frappe/socketio_client.js.
  • Les processus Python publient les événements vers le serveur Node par un canal Redis pub-sub. Le serveur temps réel s’abonne à ce canal et republie vers tous les clients abonnés.
  • Le serveur temps réel est multi-tenant : tout le trafic d’un site est cloisonné par nom de site. Les espaces de noms sont créés dynamiquement sous la forme /{sitename}, où sitename est le nom du dossier du site dans le répertoire sites (ou frappe.local.site).
  • Le serveur temps réel s’appuie sur le serveur web Frappe principal pour authentifier les connexions. Le cookie SID ou l’en-tête d’autorisation est transmis et sert à vérifier que la connexion correspond à un utilisateur valide, autorisé à s’abonner aux DocTypes et documents selon ses permissions.

Salles disponibles

SalleAccès
allRejointe par défaut par tous les utilisateurs système (System User).
websiteAccessible à tout utilisateur, y compris les invités (Guest).
user:{username}Salle par utilisateur. Autorisée sans vérification de permission.
doctype:{doctype}Salle par DocType. Seuls les utilisateurs ayant la permission sur le DocType peuvent la rejoindre ; abonnement automatique à l’ouverture de la vue liste ou formulaire.
doc:{doctype}/{name}Salle par document. Seuls les utilisateurs ayant la permission sur le document peuvent la rejoindre ; abonnement automatique à l’ouverture du formulaire.

Pages liées