Live Updates in the Bryntum Gantt Chart

The revisions feature in Bryntum Gantt is a tool designed to facilitate collaboration between users. It enables live updates, allowing multiple users to work on the same project simultaneously and see each other's changes in real-time. The feature also includes an automatic conflict resolution mechanism, which handles scheduling conflicts like circular dependencies and constraint violations. This ensures that all changes are synchronized across different clients, and any conflicts that arise are resolved in a consistent manner without a central Project instance.

The feature specifies a protocol which should be implemented by the server side to handle changes and distribute them to all users. Since there are plenty of different libraries and solutions to implement messaging between server and client, we decided to leave the network layer implementation entirely to customers. To help with implementation, we describe the protocol and required behavior in a separate guide. We also provide a simple Node.js server to run the default example and help understand the protocol.

Revisions Basics

What is a revision? A revision is a complete, minimal, and necessary set of changes that transforms the state of the project from one valid state to another. This set of changes includes the input changes to the project and the output changes which result from applying input changes to the project.

Revision changes object is similar to the project changes object, but also contains the project input. Input is a set of fields that were modified (or records that were added/removed) by the user before project got recalculated. Input changes are supposed to be applied to the client project (API does this, no extra action needed), and changes are supposed to be persisted on the server. Persisted changes can be viewed by tools that do not use the Project model and can be served to a new client.

When project data is modified - task dragged, resized, edited, dependency added, etc. - the project records a revision and triggers an event which should be handled by the client application and sent to the server. The server should persist the data, generate ids, assign a revision ID, and notify every client about the new revision. Clients should receive revisions from the server and apply them to the local project.

Normally, a revision is recorded automatically, but it can also be recorded manually by disabling autoRecord on the STM and recording the transaction. The transaction should also be wrapped in a queue call to make sure the applyRevisions method will not apply the data in the middle of the transaction.

Getting Started

To get started:

  1. Download, set up, and start the WebSocket server from this repository: gantt-websocket-server.
  2. Open the realtime-updates example in the browser, specify the server address (localhost by default), and login. Every login is allowed, no password required. After login, you will see project demo data loaded and available for editing.
  3. Open the same example in another tab and see how changes are synchronized between tabs without blocking the UI or interrupting user actions.

Using the Revisions Feature

When using the revisions feature, every client (project instance) must be provided with a unique ID. It is used to distinguish clients and their revisions. The ID can be provided by the server or generated by the client. The only requirement is uniqueness. The project will use this ID for revisions created by the client.

Optionally, when initializing revisions, you can provide a starting revision ID. If omitted, base is used by default.

The revisions feature also requires several components to be properly configured to operate correctly. To enable this feature in your application, follow these steps:

  1. Enable revisions in the State Tracking Manager. This will unblock revision recording and special events.
const project = new ProjectModel({
    stm: {
        // enables revision recording and special events
        revisionsEnabled: true
    }
});
  1. Enable transactional features in Bryntum Gantt. Transactional features use a queue which is shared with the revisions feature and allows running uninterrupted functions to modify the data.
const Gantt = new Gantt({
    enableTransactionalFeatures: true,
    project
});
  1. Load project data and initialize the revisions feature.
project.load().then(() => {
    // First step is to enable the STM
    project.stm.enable();
    // After this call every change to the project data will eventually trigger an event,
    // notifying about the revision
    project.initRevisions('client-unique-id');
});
  1. Subscribe to revisionNotification event to receive the revision recorded by the project. This is the only API required to get revisions from the project. When revision is received from the project, it should be sent to the server.
project.on('revisionNotification', revision => {
    const { localRevisionId, conflictResolutionFor, changes, clientId } = revision;
    // This is an abstract API showing how to send a revision to the server
    websocketClient.sendRevision({
       localRevisionId,
       conflictResolutionFor,
       changes,
       clientId
    });
});
  1. Implement an API to receive revisions from the server and apply them to the project.
// This is an abstract API of the network layer to receive notifications from the server
websocketClient.on('revision', revision => {
    // This method will put incoming revisions to a queue and apply them as soon as the
    // project queue is unblocked. Meaning you can call this method at any time and you
    // don't need to wait for it to finish.
    project.applyRevisions(revision);
});

After following these steps, your application will be able to produce revisions and apply incoming revisions. The only remaining thing is the server implementation which persists the data and organizes revisions, and client connection implementation.

Synchronizing WBS

To synchronize WBS among clients, it is recommended to use a configuration with an ordered tree and persisted tree index:

class MyTaskModel extends TaskModel {
    static fields = [
        { name: 'parentIndex', persist: true },
        { name: 'orderedParentIndex', persist: true }
    ];
}

The task store should also be configured to use an ordered tree to calculate WBS:

const project = new ProjectModel({
    taskStore: {
        useOrderedTreeForWbs: true,
    }
});

Timezones Support

It is important to note that calendars, non-working time, and task start/end/constraint dates should either all use timezones or all not use them. For example, if clients are in different timezones, use local non-working time and use tasks with dates with timezone information, they could not agree on when certain tasks should start. If you have a task that starts on Monday and ends on Friday, every client will calculate start/end according to the local timezone and this will create a never-ending flow of messages where each project is moving the task.

One of the ways to prevent that is to disable TZ info in the task model:

class MyTaskModel extends TaskModel {
    static fields = [
        { name: 'startDate', format: 'YYYY-MM-DDTHH:mm:ss' },
        { name: 'endDate', format: 'YYYY-MM -DDTHH:mm:ss' },
        { name: 'constraintDate', format: 'YYYY-MM-DDTHH:mm:ss' }
    ];
}

Further Reading

  1. Queue guide - learn more about the Project queue.
  2. Revisions - learn more about the revisions feature.
  3. Revisions protocol - learn how to implement the server side of the revisions feature.