[]
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.
import '@mescius/spread-sheets-collaboration-addon';
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();
*/
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;
fromSnapshot: Restores the workbook state from a Snapshot.
/**
* 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;
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.
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;
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
A User concept and related APIs have been added to support user binding, permission settings, and collaboration status display. See: User .
Using Snapshot and ChangeSet, you can implement advanced features. Below are two examples:
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);
});
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));
});