Skip to content

Instantly share code, notes, and snippets.

@esafwan
Created April 28, 2024 13:17
Show Gist options
  • Save esafwan/155f3350f3f8f37768a627589b9fa2cc to your computer and use it in GitHub Desktop.
Save esafwan/155f3350f3f8f37768a627589b9fa2cc to your computer and use it in GitHub Desktop.

To connect to Frappe's WebSocket and receive real-time updates when a document of a particular DocType is saved, you can use the socket.io-client library in your React Native app. Here's an example of a single-component app that demonstrates how to connect to the WebSocket, authenticate with an API key, and receive updates for a specific DocType:

import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import io from 'socket.io-client';

const API_KEY = 'your_api_key'; // Replace with your actual API key
const SITE_NAME = 'your_site_name'; // Replace with your site name
const DOCTYPE = 'your_doctype'; // Replace with the DocType you want to monitor

const App = () => {
  const [socket, setSocket] = useState(null);
  const [documents, setDocuments] = useState([]);

  useEffect(() => {
    const initSocket = async () => {
      const host = `https://${window.location.hostname}/${SITE_NAME}`; // Replace with your Frappe server URL
      const newSocket = io(host, {
        transports: ['websocket'],
        reconnectionAttempts: 5,
        extraHeaders: {
          Authorization: `token ${API_KEY}`, // Authenticate with API key
        },
      });

      newSocket.on('connect', () => {
        console.log('Connected to Frappe WebSocket');
        newSocket.emit('doctype_subscribe', DOCTYPE); // Subscribe to the DocType
      });

      newSocket.on('disconnect', () => {
        console.log('Disconnected from Frappe WebSocket');
      });

      newSocket.on('list_update', (data) => {
        if (data.doctype === DOCTYPE) {
          // Update the documents state with the latest data
          setDocuments(data.modified);
        }
      });

      setSocket(newSocket);
    };

    initSocket();

    return () => {
      if (socket) {
        socket.disconnect();
      }
    };
  }, []);

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Updated Documents</Text>
      {documents.map((doc) => (
        <Text key={doc.name} style={styles.document}>
          {doc.name}
        </Text>
      ))}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  title: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  document: {
    fontSize: 16,
    marginVertical: 5,
  },
});

export default App;

Here's a breakdown of what's happening in the code:

  1. The app imports the necessary components and libraries, including socket.io-client.
  2. The API_KEY, SITE_NAME, and DOCTYPE constants are defined at the top. Replace their values with your actual API key, site name, and the DocType you want to monitor, respectively.
  3. The App component is defined as a functional component.
  4. The socket state variable is used to store the WebSocket instance, and the documents state variable is used to store the updated documents.
  5. The useEffect hook is used to initialize the WebSocket connection when the component mounts.
  6. Inside the initSocket function, a new WebSocket instance is created using io from the socket.io-client library. The host variable is constructed using the site name and the current hostname. The extraHeaders option is used to pass the API key for authentication.
  7. Event listeners are set up for various WebSocket events:
    • connect: Logs a message when the connection is established and subscribes to the specified DocType.
    • disconnect: Logs a message when the connection is disconnected.
    • list_update: Listens for updates related to the specified DocType and updates the documents state accordingly.
  8. The setSocket function is called to store the WebSocket instance in the socket state variable.
  9. The useEffect hook also includes a cleanup function that disconnects the WebSocket instance when the component unmounts.
  10. The component renders a list of updated document names based on the documents state.

To use this component in your React Native app, you'll need to replace the API_KEY, SITE_NAME, and DOCTYPE constants with your actual values. Additionally, ensure that you have the necessary permissions to subscribe to the specified DocType and that your API key has the required access.

Note that this example assumes you're using Frappe's WebSocket server and that the server is configured to accept WebSocket connections from your React Native app. You may need to adjust the code based on your specific Frappe setup and requirements.

To connect to Frappe's websocket in a React Native app, you can use the socket.io-client library. Here's an example of a single component app that connects to the websocket and listens for events when a document of a particular doctype is saved.

First, install the socket.io-client library:

npm install socket.io-client

Then, create a new component App.js:

import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';
import io from 'socket.io-client';

const App = () => {
  const [socket, setSocket] = useState(null);

  useEffect(() => {
    const apiKey = 'your_api_key';
    const url = 'https://your-frappe-instance.com/socket.io/';

    const socket = io(url, {
      query: { api_key: api_key },
      transports: ['websocket'],
    });

    setSocket(socket);

    return () => {
      socket.disconnect();
    };
  }, []);

  useEffect(() => {
    if (!socket) return;

    socket.on('connect', () => {
      console.log('Connected to socket server');
      socket.emit('doctype_subscribe', 'Your Doctype');
    });

    socket.on('docinfo_update', (data) => {
      console.log('Document updated:', data);
    });

    return () => {
      socket.off('connect');
      socket.off('docinfo_update');
    };
  }, [socket]);

  return (
    <View>
      <Text>Websocket connection established. Listening for events...</Text>
    </View>
  );
};

export default App;

Replace 'your_api_key' with your actual API key and 'https://your-frappe-instance.com/socket.io/' with the URL of your Frappe instance. Also, replace 'Your Doctype' with the doctype you want to listen for.

This component will connect to the websocket server when it mounts, subscribe to the specified doctype, and listen for docinfo_update events. When a document of the specified doctype is saved, the docinfo_update event will be fired, and the updated document data will be logged to the console.

To connect to Frappe's WebSocket from a React Native app, you'll need to use a WebSocket library that supports React Native. One popular option is react-native-ws.

Here's an example of a single-component app that connects to Frappe's WebSocket, authenticates using an API key, and listens for events when a document of a particular doctype is saved:

import React, { useState, useEffect } from 'react';
import { View, Text } from 'react-native';
import { WebSocket } from 'react-native-ws';

const App = () => {
  const [socket, setSocket] = useState(null);
  const [connected, setConnected] = useState(false);
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const apiUrl = 'https://your-frappe-site.com/api';
    const apiKey = 'your-api-key';
    const doctype = 'YourDoctype'; // Replace with the doctype you're interested in

    const socketUrl = `wss://your-frappe-site.com/socket.io/?EIO=3&transport=websocket&sid=${apiKey}`;
    const socketOptions = {
      headers: {
        'Authorization': `Bearer ${apiKey}`,
      },
    };

    const socket = new WebSocket(socketUrl, socketOptions);

    socket.onopen = () => {
      setConnected(true);
      console.log('Connected to WebSocket');
    };

    socket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      if (message.event === 'doc_update' && message.message.doctype === doctype) {
        setMessages((prevMessages) => [...prevMessages, message.message]);
      }
    };

    socket.onerror = (error) => {
      console.log('WebSocket error:', error);
    };

    socket.onclose = () => {
      setConnected(false);
      console.log('WebSocket connection closed');
    };

    setSocket(socket);

    return () => {
      socket.close();
    };
  }, [apiKey, doctype]);

  return (
    <View>
      <Text>Connected: {connected ? 'Yes' : 'No'}</Text>
      <Text>Messages:</Text>
      {messages.map((message, index) => (
        <Text key={index}>{message.name} updated</Text>
      ))}
    </View>
  );
};

export default App;

Here's what's happening in the code:

  1. We create a WebSocket instance with the Frappe site's socket.io URL, passing the API key in the Authorization header.
  2. We set up event listeners for open, message, error, and close events.
  3. When a message is received, we parse the JSON data and check if the event is a doc_update event for the doctype we're interested in. If so, we add the message to the messages state.
  4. We render the connection status and the list of messages received.

Note that you'll need to replace https://your-frappe-site.com with your actual Frappe site's URL, and your-api-key with a valid API key for your Frappe site. Additionally, you may need to adjust the doctype variable to match the doctype you're interested in.

Remember to install react-native-ws using npm or yarn before running the app:

npm install react-native-ws

or

yarn add react-native-ws

To connect to Frappe's WebSocket from a React Native app, you'll need to use the Socket.IO client library. Here's an example of how you can do it:

Step 1: Install Socket.IO client library

Run the following command in your React Native project:

npm install socket.io-client

Step 2: Import Socket.IO and connect to Frappe's WebSocket

Create a new JavaScript file (e.g., FrappeWebSocket.js) and add the following code:

import { io } from 'socket.io-client';

const frappeWebSocket = io('https://your-frappe-instance.com', {
  withCredentials: true,
  reconnectionAttempts: 5,
  autoConnect: true,
});

export default frappeWebSocket;

Replace https://your-frappe-instance.com with the URL of your Frappe instance.

Step 3: Authenticate with API key

To authenticate with your API key, you can use the Authorization header. Add the following code to your FrappeWebSocket.js file:

frappeWebSocket.io.opts.query = {
  api_key: 'YOUR_API_KEY',
};

Replace YOUR_API_KEY with your actual API key.

Step 4: Listen to events

To listen to events, you can use the on method provided by Socket.IO. For example, to listen to the doc_update event, add the following code:

frappeWebSocket.on('doc_update', (data) => {
  console.log('Document updated:', data);
});

Step 5: Emit events

To emit events, you can use the emit method provided by Socket.IO. For example, to emit a doc_save event, add the following code:

frappeWebSocket.emit('doc_save', {
  doctype: 'Your Doctype',
  docname: 'Your Document Name',
});

Step 6: Integrate with your React Native app

Import the FrappeWebSocket module in your React Native app and use it to connect to Frappe's WebSocket. For example:

import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';
import FrappeWebSocket from './FrappeWebSocket';

const App = () => {
  const [connected, setConnected] = useState(false);

  useEffect(() => {
    FrappeWebSocket.on('connect', () => {
      setConnected(true);
    });

    FrappeWebSocket.on('disconnect', () => {
      setConnected(false);
    });
  }, []);

  return (
    <View>
      <Text>{connected ? 'Connected to Frappe WebSocket' : 'Not connected'}</Text>
    </View>
  );
};

export default App;

This is a basic example to get you started. You'll need to modify the code to suit your specific requirements.

Remember to replace https://your-frappe-instance.com with the URL of your Frappe instance and YOUR_API_KEY with your actual API key.

1

Tutorial: Realtime Communication in Frappe Framework

Frappe uses WebSocket technology to enable real-time communication between the server and clients, such as web browsers and mobile applications. This functionality is primarily built on socket.io, a powerful library for enabling real-time, bidirectional and event-based communication. Below, we'll explore how Frappe implements this feature across different components and how it can be utilized in various types of applications.

Overview of Components

  1. Backend (Python): Frappe provides a Python module to handle realtime notifications, progress updates, and more.
  2. Node.js Server: A separate Node.js server uses socket.io to handle WebSocket connections and route messages.
  3. Frontend (JavaScript): The Frappe JavaScript library integrates with the WebSocket server to receive and send messages.

1. Backend Implementation (Python)

On the server side, Frappe uses Python to publish messages to a Redis queue, which the Node.js server then subscribes to. Here’s how it works:

Publishing Events:

# frappe/realtime.py

import frappe
from frappe.utils.background_jobs import get_redis_connection

def publish_realtime(event, message, user=None):
    if user:
        room = f'user:{user}'
    else:
        room = 'global'  # Broadcast to all users

    r = get_redis_connection()
    r.publish('events', frappe.as_json({
        'event': event,
        'message': message,
        'room': room
    }))

This function pushes the event data to Redis, which the Node.js server listens to.

2. Node.js Server

The Node.js server listens for messages from Redis and forwards them to the appropriate clients through socket.io.

Node.js Server Setup:

// frappe/realtime/index.js

const { Server } = require("socket.io");
const Redis = require("ioredis");

const io = new Server();
const redis = new Redis();

redis.subscribe('events');
redis.on('message', (channel, message) => {
    message = JSON.parse(message);
    io.to(message.room).emit(message.event, message.message);
});

io.on('connection', socket => {
    console.log("A user connected");
    socket.on('join', (room) => {
        socket.join(room);
    });
});

io.listen(3000);

3. Frontend Implementation (JavaScript)

On the client side, Frappe uses the socket.io-client to connect to the WebSocket server.

JavaScript Client:

// frappe/public/js/frappe/socketio_client.js

import { io } from "socket.io-client";

const socket = io('http://localhost:3000');

socket.on('connect', () => {
    console.log('Connected to Frappe WebSocket server!');
    socket.emit('join', 'global');
});

socket.on('realtime_event', data => {
    console.log('Received data:', data);
});

Usage in Different Contexts

Frappe Desk Pages: For integrating real-time features on desk pages, the client script connects to the WebSocket server and listens for specific events related to the document or operation.

Webpages & SPA (Single Page Applications) like React: In a React application, you can use the same socket.io-client approach to connect and subscribe to events.

React Native: The implementation in React Native is similar to React. Ensure you correctly handle the connection and state updates due to the asynchronous nature of real-time events.

Creating and Listening to Custom Events

To create and listen to custom events, define your event types and ensure both the backend and frontend handle them appropriately.

Backend (Python) Custom Event:

# Publish a custom event
publish_realtime('custom_event', {'detail': 'Example data'}, user='User1')

Frontend (JavaScript) Listen to Custom Event:

socket.on('custom_event', data => {
    console.log('Received custom event data:', data);
});

Summary

Frappe's real-time communication setup uses a combination of Python for event generation, Node.js for managing WebSocket connections, and JavaScript for handling client-side interactions. This setup enables efficient, scalable, and real-time updates across various components of Frappe applications, enhancing the user experience in desk pages, webpages, and mobile apps.

2

Expanded Examples: Using Frappe Real-Time Events in Different Applications

To leverage Frappe's real-time capabilities across different platforms, you'll need to integrate the socket.io-client. Here, I'll provide examples for React, React Native, and a custom HTML page. Additionally, I'll cover how to authenticate these connections.

Common Setup: Node.js and Python

Ensure your Node.js WebSocket server and Python publish methods are configured as described previously. Authentication often involves sending a session token or an API key that your WebSocket server can verify.

Example in React

Let's start by creating a React component that subscribes to a specific doctype's updates.

import React, { useEffect, useState } from 'react';
import io from 'socket.io-client';

function DocumentUpdates() {
  const [documents, setDocuments] = useState([]);

  useEffect(() => {
    const socket = io('http://your-frappe-server.com', {
      withCredentials: true,
      extraHeaders: {
        Authorization: `Bearer your_api_token_here`
      }
    });

    socket.on('connect', () => {
      console.log('Connected!');
      socket.emit('join', 'doctype:Invoice'); // Subscribe to Invoice doctype
    });

    socket.on('doctype_update', data => {
      console.log('Update received:', data);
      setDocuments(docs => [...docs, data]);
    });

    return () => socket.disconnect();
  }, []);

  return (
    <div>
      <h1>Document Updates</h1>
      {documents.map((doc, index) => (
        <div key={index}>
          <pre>{JSON.stringify(doc, null, 2)}</pre>
        </div>
      ))}
    </div>
  );
}

export default DocumentUpdates;

Example in React Native

For React Native, the setup is similar but ensures you handle mobile-specific considerations like network changes.

import React, { useEffect, useState } from 'react';
import { View, Text, ScrollView } from 'react-native';
import io from 'socket.io-client';

function DocumentUpdates() {
  const [documents, setDocuments] = useState([]);

  useEffect(() => {
    const socket = io('http://your-frappe-server.com', {
      jsonp: false, // important for server-side communication
      withCredentials: true,
      extraHeaders: {
        Authorization: `Bearer your_api_token_here`
      }
    });

    socket.on('connect', () => {
      console.log('Connected!');
      socket.emit('join', 'doctype:Invoice');
    });

    socket.on('doctype_update', data => {
      console.log('Update received:', data);
      setDocuments(docs => [...docs, data]);
    });

    return () => socket.disconnect();
  }, []);

  return (
    <ScrollView>
      <Text style={{ fontSize: 18, fontWeight: 'bold' }}>Document Updates</Text>
      {documents.map((doc, index) => (
        <Text key={index} style={{ padding: 10 }}>
          {JSON.stringify(doc, null, 2)}
        </Text>
      ))}
    </ScrollView>
  );
}

export default DocumentUpdates;

Example in Custom HTML

For a simple HTML page, include the Socket.IO client library.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Real-Time Document Updates</title>
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
  const socket = io('http://your-frappe-server.com', {
    withCredentials: true,
    extraHeaders: {
      Authorization: 'Bearer your_api_token_here'
    }
  });

  socket.on('connect', function() {
    console.log('Connected!');
    socket.emit('join', 'doctype:Invoice');
  });

  socket.on('doctype_update', function(data) {
    console.log('Document update:', data);
    const element = document.createElement('pre');
    element.textContent = JSON.stringify(data, null, 2);
    document.body.appendChild(element);
  });

  socket.on('disconnect', function() {
    console.log('Disconnected!');
  });
});
</script>
</head>
<body>
<h1>Real-Time Document Updates</h1>
</body>
</html>

Authentication

For authentication:

  • React/React Native: Pass the authentication token through extraHeaders or as part of the connection query. Ensure your WebSocket server is configured to validate these tokens.
  • Custom HTML/JavaScript: Similarly, pass tokens via headers or connection queries. Make sure the browser and server are configured to handle CORS issues, especially with credentials and headers.

Final Note

Each platform needs specific attention to network handling and UI updating mechanisms, especially for real-time data which may update frequently and unpredictably.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment