[]
        
(Showing Draft Content)

SpreadJS Sheets Collaboration Add-on

In real-time collaborative editing, Snapshot and ChangeSet are two core concepts introduced to ensure data consistency, efficiency, and real-time performance when multiple users edit the same document simultaneously. These concepts address critical challenges such as concurrent operations, data synchronization, and performance optimization. Below, we’ll explore their definitions, purposes, and implementation details.

Installation

import '@mescius/spread-sheets-collaboration-addon';

Collaboration Class

In order to provide collaborative capabilities, a property is added to the workbook to obtain the Collaboration class, which provides collaborative related capabilities.

///* field GC.Spread.Sheets.@Workbook.collaboration: GC.Spread.Sheets.Collaboration.Collaboration
/**
 * Collaboration manager for the workbook.
 * @type {GC.Spread.Sheets.Collaboration.Collaboration}
 * //This example shows how to get snapshot.
 * @example
 * var snapshot = workbook.collaboration.toSnapshot();
 */

Snapshot

Why Use Snapshot

  • In a multi-user environment, every user needs to know the current state of the document. A Snapshot represents the complete state of the document at a specific version.

  • Snapshot allows client to quickly retrieve the latest document version, serving as a baseline for subsequent edits.

Value in Collaboration

  • Consistency: Snapshots provide a unified starting point for all users, ensuring everyone edits based on the same document state.

  • Initialization and Recovery: New users or those reconnecting after a disconnection can sync to the latest state using a snapshot.

Snapshot Format

Snapshot uses a new data format designed specifically for real-time collaboration, distinct from previous formats like ssjson or sjs.

API

A Snapshot captures the full state of a workbook at a specific version. The following methods enable conversion between a Workbook instance and a Snapshot:

  • toSnapshot: Saves the workbook state as a Snapshot.

/**
 * Only used in collaboration case, to save workbook state to snapshot.
 * @return {object} snapshot - snapshot object
 */
Workbook.collaboration.prototype.toSnapshot = function (): any;
/**
 * Only used in collaboration case, to restore the snapshot to workbook state.
 * @param {object} snapshot - snapshot object
 */
Workbook.collaboration.prototype.fromSnapshot = function (snapshot: any): void;

Op (Operation)

What is a Op

Every modification to the document model is recorded as an Op (Operation).

Why Use Op

  • Transmitting the entire document (a new snapshot) after every edit would consume excessive bandwidth. Ops describe specific changes that lead to a new snapshot, such as setValue (setting a value) or addRow (adding a row).

  • By sending only Op, the system achieves synchronization without repeatedly transmitting the full document.

API

Each Op includes a type field to describe the type of modification:

export interface IOpComponent{
    type: GC.Spread.Sheets.Collaboration.OpType;
}

For detailed OpType definitions, refer to the documentation: Op Type.

ChangeSet

What is ChangeSet

  • A ChangeSet is a collection of related Ops, typically representing a single logical unit of modification. It describes how a document transitions from one version to another.

  • In SpreadJS, Ops generated by a single command are grouped into a ChangeSet.

Why Use ChangeSet

  • Logical Grouping: ChangeSet organize related Ops, making modifications more coherent. For example, a single action might affect multiple models, and submitting it as a ChangeSet is clearer than Ops.

  • Atomicity: ChangeSet ensure that a group of Ops is either fully applied or not applied at all, maintaining data consistency and avoiding partial states.

  • Conflict Resolution: In multi-user scenarios, edits may conflict. ChangeSets provide a higher-level abstraction, enabling the system to compare and merge changes efficiently.

  • History Tracking: ChangeSet record the full modification history, supporting change tracking, rollback to specific versions, and debugging or auditing.

  • Performance Optimization: Bundling multiple Ops into a ChangeSet is more efficient for network transmission and synchronization, especially under limited bandwidth.

Value in Collaboration

  • Efficiency: ChangeSet is compact and fast to transmit, ideal for real-time collaboration.

  • Concurrency Handling: Using Operational Transformation (OT), ChangeSet manages simultaneous user operations, ensuring consistent outcomes.

ChangeSet Limitation

Only workbooks originating from the same Snapshot are considered the same document and can apply each other’s Ops.

API

The collaboration provides two APIs for applying and monitoring ChangeSet:

  • applyChangeSet: Applies a ChangeSet to the document.

    interface IChangeSet {
        ops: IOpComponent[];
    }
    /**
     * Used only in collaboration scenarios to apply a changeset to the document.
     * @param {changeSet: GC.Spread.Sheets.Collaboration.IChangeSet} changeSet - change set
     */
    Workbook.collaboration.prototype.applyChangeSet = (changeSet: GC.Spread.Sheets.Collaboration.IChangeSet) => void;
  • onChangeSet: Listens for ChangeSet generation.

    /**
     * Used only in collaboration scenarios to listen for changesets.
     * @param {changeSetHandler: GC.Spread.Sheets.Collaboration.IChangeSetHandler} onOpHandler - callback to watch change set
     */
    Workbook.collaboration.prototype.onChangeSet = (onChangeSetHandler: GC.Spread.Sheets.Collaboration.IChangeSetHandler) => void;

Others

Register Types

Registering collaboration types is a required step in collaborative scenarios to avoid issues like abnormal undo behavior. The workbook includes a method to register an OT_Type for handling additional Op information (e.g., conflict resolution):

API

export interface IOT_Type{
    uri?: string;
    transform?: (op1: GC.Spread.Sheets.Collaboration.IChangeSet, op2: GC.Spread.Sheets.Collaboration.IChangeSet, side: 'left' | 'right') => GC.Spread.Sheets.Collaboration.IChangeSet;
    transformX?: (op1: GC.Spread.Sheets.Collaboration.IChangeSet, op2: GC.Spread.Sheets.Collaboration.IChangeSet, side: 'left' | 'right') => GC.Spread.Sheets.Collaboration.IChangeSet[];
}
/**
 * Only used in collaboration case, to register collaboration type.
 * @param {GC.Spread.Sheets.Collaboration.IOT_Type} type - collaboration type
 */
Workbook.collaboration.prototype.registerCollaborationType(type: GC.Spread.Sheets.Collaboration.IOT_Type): void;

Example

import * as GC from '@mescius/spread-sheets'
import { type } from '@mescius/spread-sheets-collaboration-client';

const workbook = new GC.Spread.Sheets.Workbook('ss');
workbook.collaboration.registerCollaborationType(type);

For more details, see: SpreadJS Sheets Collaboration packages OT_Type

User

A User concept and related APIs have been added to support user binding, permission settings, and collaboration status display. See: User .

Example Implementations

Using Snapshot and ChangeSet, you can implement advanced features. Below are two examples:

Example 1: Sync Two Spread Instances on a Page

Use onChangeSet to monitor model changes and sync two Spread instances in real time:

var spread = new GC.Spread.Sheets.Workbook(document.getElementById('ss'));
var spread2 = new GC.Spread.Sheets.Workbook(document.getElementById('ss2'));
   
spread2.collaboration.fromSnapshot(spread.collaboration.toSnapshot());
spread.collaboration.onChangeSet((changeSet) => {
  spread2.collaboration.applyChangeSet(changeSet);
});
spread2.collaboration.onChangeSet((changeSet) => {
  spread.collaboration.applyChangeSet(changeSet);
});

Example 2: Store and Restore State

Listen for ChangeSets and store them in localStorage to restore the state after a page refresh:

// Initialize SpreadJS Workbook
var spread = new GC.Spread.Sheets.Workbook(document.getElementById('ss'));

// Store initial Snapshot
const initialSnapshot = spread.collaboration.toSnapshot();
if (!localStorage.getItem('snapshot')) {
   localStorage.setItem('snapshot', JSON.stringify(initialSnapshot)); 
}
applyChangeSet(spread);

// Apply stored snapshot and changeSets from localStorage
function applyChangeSet (spread) {
    // get snapshot and changes from localStorage 
    const storedSnapshot = JSON.parse(localStorage.getItem('snapshot'));
    const storedChanges = JSON.parse(localStorage.getItem('changes') || '[]');

    if (storedSnapshot && spread) {
        spread.collaboration.fromSnapshot(storedSnapshot);

        spread.suspendPaint();
        // apply changeSet
        storedChanges.forEach((changeSet) => {
            spread.collaboration.applyChangeSet(changeSet);
        });
        spread.resumePaint();
    }
}

// Listen for and store changeSets
spread.collaboration.onChangeSet((changeSet) => {
    // get changes from localStorage,default is []
    let changes = JSON.parse(localStorage.getItem('changes') || '[]');
    // add new changeSet
    changes.push(changeSet);
    // save to localStorage
    localStorage.setItem('changes', JSON.stringify(changes));
});