odoo16 front-end framework source code reading–rpc_service.js

odoo16 front-end framework source code reading–rpc_service.js

Let me first introduce some background knowledge to make it easier to read the code.

1. JSONRPC specification

https://www.jsonrpc.org/specification

Chinese translation version: https://wiki.geekdream.com/Specification/json-rpc_2.0.html

JSON-RPC is a stateless and lightweight remote procedure call (RPC) protocol. This specification mainly defines some data structures and their related processing rules. It allows running in the same process based on many different message transmission environments such as socket, http, etc. It uses JSON (RFC 4627) as the data format.

It’s made for simplicity!

Since JSON-RPC uses JSON, it has the same type system as it (see http://www.json.org or RFC 4627). JSON can represent four basic types (String, Numbers, Booleans, and Null) and two structured types (Objects and Arrays). In the specification, the term “Primitive” marks the four primitive types, and “Structured” marks the two structured types. Any time a document refers to a JSON data type, the first letter must be capitalized: Object, Array, String, Number, Boolean, Null. Including True and False should also be capitalized.

1. Request object

Sending a request object to the server represents an RPC call. A request object contains the following members:

jsonrpc

A string specifying the JSON-RPC protocol version, which must be written exactly as “2.0”

method

A string containing the name of the method to be called. The method name starting with rpc, connected with an English period (U+002E or ASCII 46), is the method name and extension reserved for rpc internals, and cannot be used elsewhere.

params

The structured parameter value required to call the method. This member parameter can be omitted.

id

The unique identification id of the client has been established. The value must contain a string, numeric value or NULL value. If this member is not included, it is considered a notification. The value is generally not NULL[1]. If it is a numerical value, it should not contain decimals[2].

The server MUST reply with the same value if included in the response object. This member is used to associate context between two objects.

A request object that does not contain an “id” member is a notification. A request object that is a notification indicates that the client is not interested in the corresponding response object.

 const data = {
        id: rpcId,
        jsonrpc: "2.0",
        method: "call",
        params: params,
    };

2. Response object

When making an rpc call, the server must reply with a response in addition to notifications. The response is represented as a JSON object, using the following members:

jsonrpc

A string specifying the JSON-RPC protocol version, which must be written exactly as “2.0”

result

This member must be included on success.

This member must not be included when calling a method that causes an error.

The called method on the server determines the value of this member.

error

This member must be included on failure.

This member must not be included when no error is caused.

The member parameter value must be an object defined in 5.1.

id

This member must be included.

The value of this member must be consistent with the value of the id member in the request object.

If there is an error when checking the request object id (such as wrong parameters or invalid request), the value must be empty.

The response object must contain either a result or an error member, but not both.

3. Error object

When an rpc call encounters an error, the returned response object must contain the error member parameters and be an object with the following member parameters:

code

Use a numeric value to represent the error type of this exception. Must be an integer.

message

A simple string describing the error. The description should be limited to one sentence.

data

A basic or structured type that contains additional information about the error. This member can be ignored. The member value is defined by the server (such as detailed error information, nested errors, etc.).

code message meaning
-32700 Parse error Syntax parsing error The server received invalid json. This error is sent when the server tries to parse the json text
-32600 Invalid Request The json sent is not a valid request object.
-32601 Method not found The method does not exist or is invalid.
-32602 Invalid params Invalid parameters Invalid method parameters.
-32603 Internal error JSON-RPC internal error.
-32000 to -32099 Server error Server error Reserved for custom server errors.

2. rpc_service.js

Path: addons\web\static\src\core\
etwork\rpc_service.js

1. Introduce relevant modules and create new related Error classes

Two js modules are imported, browser is probably related to the browser

registry is the front-end registry

Then four Errors are defined to inherit their own standard class Error

  • RPCError
  • ConnectionLostError
  • ConnectionAbortedError
  • HTTPError
/** @odoo-module **/

import {<!-- --> browser } from "../browser/browser";
import {<!-- --> registry } from "../registry";

//------------------------------------------------ --------------------------
// Errors
//------------------------------------------------ --------------------------
export class RPCError extends Error {<!-- -->
    constructor() {<!-- -->
        super(...arguments);
        this.name = "RPC_ERROR";
        this.type = "server";
        this.code = null;
        this.data = null;
        this.exceptionName = null;
        this.subType = null;
    }
}

export class ConnectionLostError extends Error {<!-- -->}

export class ConnectionAbortedError extends Error {<!-- -->}

export class HTTPError extends Error {<!-- -->}

2. Error object

Return an RPCError based on the response value. This structure assignment is quite interesting.

const {<!-- --> code, data: errorData, message, type: subType } = response;

I guess so. The four attributes code, data, message, and type in the response are assigned to code, errorData, message, and subType respectively. Two variable names have been replaced. Is it necessary?

//------------------------------------------------ --------------------------------
// Main RPC method
//------------------------------------------------ --------------------------
export function makeErrorFromResponse(reponse) {<!-- -->
    // Odoo returns error like this, in a error field instead of properly
    // using http error codes...
    
    const error = new RPCError();
    error.exceptionName = errorData.name;
    error.subType = subType;
    error.data = errorData;
    error.message = message;
    error.code = code;
    return error;
}


3. Define relevant variables

5 input parameters

  1. env front-end environment, it seems that this object can be used directly in the front-end js code.
  2. rpcId id, in order to correspond to the response, this id is needed
  3. url address
  4. params parameter value
  5. setting defaults to an empty object
export function jsonrpc(env, rpcId, url, params, settings = {<!-- -->}) {<!-- -->
    const bus = env.bus;
    const XHR = browser.XMLHttpRequest;
    const data = {<!-- -->
        id: rpcId,
        jsonrpc: "2.0",
        method: "call",
        params: params,
    };
    const request = settings.xhr || new XHR();
    let rejectFn;

There may seem to be a lot of parameters. In fact, when encapsulating it into a service later, the first two parameters do not need to be passed. Only the last three parameters need to be passed. In fact, params and settings do not need to be passed. The only thing that must be passed is the url.

bus: Bus, some signals need to be sent through the bus here. After all, rpc is a remote call, and unexpected situations will inevitably occur, so bus is required for communication.

XHR: The browser initiates a request request

data: standard jsonrpc data format

rejectFn, it is worth mentioning here that this variable is declared here, but no value is assigned, for later use.

4. Review promise

Promise is a new solution for asynchronous programming introduced in ES6. Syntactically, Promise is a constructor that encapsulates asynchronous operations and can obtain success or failure results.

// new Promise generates an asynchronous task, the parameter is the function that specifically executes the task, and receives two parameters
//Generally, resolve and reject are both functions. The former is called if the asynchronous task is executed successfully, otherwise the latter is called.
// These two methods will change the state of the p object and pass data to the next step of processing.
// Then call p.then and receive two function parameters, corresponding to the successful callback and failed callback of the asynchronous task.
const p = new Promise(function(resolve,reject){<!-- -->
    setTimeout(function () {<!-- -->
        // let data="User data in database";
        // resolve(data);
        let err="Data reading failed";
        reject(err);
    },1000);
})

p.then(function(value){<!-- -->
    console.log(value);
},function(reason){<!-- -->
    console.error(reason);
})

//In fact, promises also solve the problem of callback hell and too much nesting, by encapsulating asynchronous tasks into objects.

In fact, promise is completed in two steps. The object itself only performs an asynchronous task and does not process the asynchronous return results. The results returned by the asynchronous task are processed in the p.then function.
The return value of the asynchronous task will affect the state of the promise object itself and determine which callback function is executed in p.then.

In addition, promises can be called in chains to execute serial asynchronous tasks.

5. Bus in promise

Many messages are sent to the bus in promise

 if (!settings.silent) {
            bus.trigger("RPC:REQUEST", data.id);
        }

Since it has been sent here, there must be a place to receive it. Search RPC: REQUEST in odoo.

addons\web\static\src\webclient\loading_indicator\loading_indicator.js

The code of this file is not long, just paste it here. The comments roughly mean:

Loading indicator:

When the user performs an action, it is best to give him some feedback that something is happening. The function of this indicator is to display a small rectangular box in the lower right corner of the screen with the word Loading inside and the rpc id. After 3 seconds, if the rpc is still not completed, we will block the entire UI. Go back and test it.

/** @odoo-module **/

import {<!-- --> browser } from "@web/core/browser/browser";
import {<!-- --> registry } from "@web/core/registry";
import {<!-- --> useService } from "@web/core/utils/hooks";
import {<!-- --> Transition } from "@web/core/transition";

import {<!-- --> Component, onWillDestroy, useState } from "@odoo/owl";

/**
 * Loading Indicator
 *
 * When the user performs an action, it is good to give him some feedback that
 * something is currently happening. The purpose of the Loading Indicator is to
 * display a small rectangle on the bottom right of the screen with just the
 * text 'Loading' and the number of currently running rpcs.
 *
 * After a delay of 3s, if a rpc is still not completed, we also block the UI.
 */
export class LoadingIndicator extends Component {<!-- -->
    setup() {<!-- -->
        this.uiService = useService("ui");
        this.state = useState({<!-- -->
            count: 0,
            show: false,
        });
        this.rpcIds = new Set();
        this.shouldUnblock = false;
        this.startShowTimer = null;
        this.blockUITimer = null;
        this.env.bus.addEventListener("RPC:REQUEST", this.requestCall.bind(this));
        this.env.bus.addEventListener("RPC:RESPONSE", this.responseCall.bind(this));
        onWillDestroy(() => {<!-- -->
            this.env.bus.removeEventListener("RPC:REQUEST", this.requestCall.bind(this));
            this.env.bus.removeEventListener("RPC:RESPONSE", this.responseCall.bind(this));
        });
    }

    requestCall({<!-- --> detail: rpcId }) {<!-- -->
        if (this.state.count === 0) {<!-- -->
            browser.clearTimeout(this.startShowTimer);
            this.startShowTimer = browser.setTimeout(() => {<!-- -->
                if (this.state.count) {<!-- -->
                    this.state.show = true;
                    this.blockUITimer = browser.setTimeout(() => {<!-- -->
                        this.shouldUnblock = true;
                        this.uiService.block();
                    }, 3000);
                }
            }, 250);
        }
        this.rpcIds.add(rpcId);
        this.state.count + + ;
    }

    responseCall({<!-- --> detail: rpcId }) {<!-- -->
        this.rpcIds.delete(rpcId);
        this.state.count = this.rpcIds.size;
        if (this.state.count === 0) {<!-- -->
            browser.clearTimeout(this.startShowTimer);
            browser.clearTimeout(this.blockUITimer);
            this.state.show = false;
            if (this.shouldUnblock) {<!-- -->
                this.uiService.unblock();
                this.shouldUnblock = false;
            }
        }
    }
}

LoadingIndicator.template = "web.LoadingIndicator";
LoadingIndicator.components = {<!-- --> Transition };

registry.category("main_components").add("LoadingIndicator", {<!-- -->
    Component: LoadingIndicator,
});

6. Request binding load event

The request is bound to a load event, which is the operation triggered when the request returns.

 request.addEventListener("load", () => {<!-- -->
            if (request.status === 502) {<!-- -->
                // If Odoo is behind another server (eg.: nginx)
                if (!settings.silent) {<!-- -->
                    bus.trigger("RPC:RESPONSE", data.id);
                }
                reject(new ConnectionLostError());
                return;
            }
            let params;
            try {<!-- -->
                params = JSON.parse(request.response);
            } catch (_) {<!-- -->
                // the response isn't json parsable, which probably means that the rpc request could
                // not be handled by the server, e.g. PoolError('The Connection Pool Is Full')
                if (!settings.silent) {<!-- -->
                    bus.trigger("RPC:RESPONSE", data.id);
                }
                return reject(new ConnectionLostError());
           
            const {<!-- --> error: responseError, result: responseResult } = params;
            if (!settings.silent) {<!-- -->
                bus.trigger("RPC:RESPONSE", data.id);
            }
            if (!responseError) {<!-- -->
                return resolve(responseResult);
            }
            const error = makeErrorFromResponse(responseError);
            reject(error);
        });

6.1. What the hell is status = 502?

A 502 Bad Gateway error means that the proxy or gateway received an invalid or incomplete response from the previous server. Typically, this happens on high-traffic websites where the files are too large or the processing speed is too slow. For example, when you visit a website with high traffic, your request will be sent to its proxy server. If the proxy server cannot get a complete response from the upstream server when trying to access the website, it will generate a 502 error code.

502 error codes are usually caused by devices such as proxy servers, gateways, or load balancers, rather than your computer or network connection. This means you can only make limited adjustments to your network connection, but you can’t fix gateway response errors.

The comments also make it clear that 502 may be due to the use of nginx reverse proxy, and 502 errors are caused by poor communication between nginx and odoo. In this case, rpc execution fails.

execute this sentence

 reject(new ConnectionLostError());

6.2 Parsing return values

If the data returned is not in json format, an error will also be triggered.

 try {<!-- -->
        params = JSON.parse(request.response);
     } catch (_) {<!-- -->
                return reject(new ConnectionLostError());
                 }

What the hell is that underscore after catch? It may be that you don’t care what error occurred, as long as there is an error in parsing, call reject

6.3 Destructuring assignment

const {<!-- --> error: responseError, result: responseResult } = params;

According to the jsonrpc specification, error and result must and can only return one.

I also made a judgment later

 if (!responseError) {<!-- -->
            return resolve(responseResult);
        }

If there are no errors, call resolve and return. Otherwise, an error occurs. First generate an error and then call reject.

 const error = makeErrorFromResponse(responseError);
            reject(error);

7. Request binding error event

 request.addEventListener("error", () => {<!-- -->
            if (!settings.silent) {<!-- -->
                bus.trigger("RPC:RESPONSE", data.id);
            }
            reject(new ConnectionLostError());
        });

8. It’s time to get down to business

 request.open("POST", url);
        request.setRequestHeader("Content-Type", "application/json");
        request.send(JSON.stringify(data));

Three lines of code:

1. Why not use get when requesting the url using the post method? Because post is more secure.

2. Content-Type is specified as json. This is very important. If not specified, the server does not know how to parse the data.

3. Convert the data into a string and send it out. (I have been busy for a long time just for this sentence)

9. Promise.abort is defined

Promise has only three states: pending, resolve, and reject. Once an asynchronous promise is issued, after waiting (pending), it can only succeed or fail in the end, and cannot be canceled midway (abort).

Promise.abort is defined here, and the comments are also clear. Allow users to cancel ignored rpc requests to avoid blocking the UI and not display errors.

 /**
     * @param {Boolean} rejectError Returns an error if true. Allows you to cancel
     * ignored rpc's in order to unblock the ui and not display an error.
     */
    promise.abort = function (rejectError = true) {
        if (request.abort) {
            request.abort();
        }
        if (!settings.silent) {
            bus.trigger("RPC:RESPONSE", data.id);
        }
        if (rejectError) {
            rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort"));
        }
    };

10, jsonrpc

This function does one thing, defines a promise object to send the rpc request and returns it.

export function jsonrpc(env, rpcId, url, params, settings = {<!-- -->}) {<!-- -->
    const bus = env.bus;
    const XHR = browser.XMLHttpRequest;
    const data = {<!-- -->
        id: rpcId,
        jsonrpc: "2.0",
        method: "call",
        params: params,
    };
    const request = settings.xhr || new XHR();
    let rejectFn;
    const promise = new Promise((resolve, reject) => {
        rejectFn = reject;
        //handle success
        request.addEventListener("load", () => {

        });
        //handle failure
        request.addEventListener("error", () => {

        });
   
        request.open("POST", url);
        request.setRequestHeader("Content-Type", "application/json");
        request.send(JSON.stringify(data));
    });

    promise.abort = function (rejectError = true) {

    };
    return promise;
}

11. Define RPC service

Here, jsonrpc is further encapsulated and registered as a service. It seems that each service has a start function and env is passed in as a parameter.

//------------------------------------------------ --------------------------------
// RPC service
//------------------------------------------------ --------------------------
export const rpcService = {<!-- -->
    async: true,
    start(env) {<!-- -->
        let rpcId = 0;
        return function rpc(route, params = {<!-- -->}, settings) {<!-- -->
            return jsonrpc(env, rpcId + + , route, params, settings);
        };
    },
};

registry.category("services").add("rpc", rpcService);

Appendix: odoo16 rpc_service.js

/** @odoo-module **/

import {<!-- --> browser } from "../browser/browser";
import {<!-- --> registry } from "../registry";

//------------------------------------------------ --------------------------
// Errors
//------------------------------------------------ --------------------------
export class RPCError extends Error {<!-- -->
    constructor() {<!-- -->
        super(...arguments);
        this.name = "RPC_ERROR";
        this.type = "server";
        this.code = null;
        this.data = null;
        this.exceptionName = null;
        this.subType = null;
    }
}

export class ConnectionLostError extends Error {<!-- -->}

export class ConnectionAbortedError extends Error {<!-- -->}

export class HTTPError extends Error {<!-- -->}

//------------------------------------------------ --------------------------
// Main RPC method
//------------------------------------------------ --------------------------
export function makeErrorFromResponse(reponse) {
    // Odoo returns error like this, in a error field instead of properly
    // using http error codes...
    const {<!-- --> code, data: errorData, message, type: subType } = response;
    const error = new RPCError();
    error.exceptionName = errorData.name;
    error.subType = subType;
    error.data = errorData;
    error.message = message;
    error.code = code;
    return error;
}

export function jsonrpc(env, rpcId, url, params, settings = {<!-- -->}) {<!-- -->
    const bus = env.bus;
    const XHR = browser.XMLHttpRequest;
    const data = {<!-- -->
        id: rpcId,
        jsonrpc: "2.0",
        method: "call",
        params: params,
    };
    const request = settings.xhr || new XHR();
    let rejectFn;
    const promise = new Promise((resolve, reject) => {
        rejectFn = reject;
        if (!settings.silent) {
            bus.trigger("RPC:REQUEST", data.id);
        }
        //handle success
        request.addEventListener("load", () => {
            if (request.status === 502) {
                // If Odoo is behind another server (eg.: nginx)
                if (!settings.silent) {
                    bus.trigger("RPC:RESPONSE", data.id);
                }
                reject(new ConnectionLostError());
                return;
            }
            let params;
            try {
                params = JSON.parse(request.response);
            } catch (_) {
                // the response isn't json parsable, which probably means that the rpc request could
                // not be handled by the server, e.g. PoolError('The Connection Pool Is Full')
                if (!settings.silent) {
                    bus.trigger("RPC:RESPONSE", data.id);
                }
                return reject(new ConnectionLostError());
            }
            const {<!-- --> error: responseError, result: responseResult } = params;
            if (!settings.silent) {
                bus.trigger("RPC:RESPONSE", data.id);
            }
            if (!responseError) {
                return resolve(responseResult);
            }
            const error = makeErrorFromResponse(responseError);
            reject(error);
        });
        //handle failure
        request.addEventListener("error", () => {<!-- -->
            if (!settings.silent) {<!-- -->
                bus.trigger("RPC:RESPONSE", data.id);
            }
            reject(new ConnectionLostError());
        });
        // configure and send request
        request.open("POST", url);
        request.setRequestHeader("Content-Type", "application/json");
        request.send(JSON.stringify(data));
    });
    /**
     * @param {Boolean} rejectError Returns an error if true. Allows you to cancel
     * ignored rpc's in order to unblock the ui and not display an error.
     */
    promise.abort = function (rejectError = true) {
        if (request.abort) {
            request.abort();
        }
        if (!settings.silent) {
            bus.trigger("RPC:RESPONSE", data.id);
        }
        if (rejectError) {
            rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort"));
        }
    };
    return promise;
}

//------------------------------------------------ --------------------------
// RPC service
//------------------------------------------------ --------------------------
export const rpcService = {
    async: true,
    start(env) {
        let rpcId = 0;
        return function rpc(route, params = {}, settings) {
            return jsonrpc(env, rpcId + + , route, params, settings);
        };
    },
};

registry.category("services").add("rpc", rpcService);