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.
À 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 :
frappe.realtime.on('event_name', (data) => {
console.log(data)
})frappe.realtime.off
Pour cesser d’écouter un événement auquel vous vous étiez abonné :
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 :
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 :
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).
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ètre | Valeur par défaut | Rôle |
|---|---|---|
frappe_context | False | Ouvre un contexte Frappe complet (base de données + session) pour le corps du gestionnaire. |
allow_guest | False | Si False, l’événement est ignoré quand socket.user == "Guest". |
Exemple
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 :
@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 :
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 :
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 appsSalles (rooms), émission et état transitoire :
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 / attribut | Rôle |
|---|---|
socket.site | Le site auquel ce socket est rattaché. |
socket.user | L’utilisateur connecté ; "Guest" pour un anonyme. |
socket.user_type | Le type d’utilisateur, p. ex. "System User". |
socket.installed_apps | La 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.
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.
@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 » :
// 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.
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 :
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
Authorizationcomme 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ùsitenameest le nom du dossier du site dans le répertoiresites(oufrappe.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
| Salle | Accès |
|---|---|
all | Rejointe par défaut par tous les utilisateurs système (System User). |
website | Accessible à 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. |
