@jupyter/ydoc
Version:
Jupyter document structures for collaborative editing using YJS
251 lines (218 loc) • 5.21 kB
text/typescript
/* -----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
import { JSONExt, JSONObject, JSONValue } from '@lumino/coreutils';
import { ISignal, Signal } from '@lumino/signaling';
import { Awareness } from 'y-protocols/awareness';
import * as Y from 'yjs';
import type { DocumentChange, ISharedDocument, StateChange } from './api.js';
/**
* Generic shareable document.
*/
export abstract class YDocument<T extends DocumentChange>
implements ISharedDocument
{
constructor(options?: YDocument.IOptions) {
this._ydoc = options?.ydoc ?? new Y.Doc();
this._ystate = this._ydoc.getMap('state');
this._undoManager = new Y.UndoManager([], {
trackedOrigins: new Set([this]),
doc: this._ydoc
});
this._awareness = new Awareness(this._ydoc);
this._ystate.observe(this.onStateChanged);
}
/**
* Document version
*/
abstract readonly version: string;
/**
* YJS document.
*/
get ydoc(): Y.Doc {
return this._ydoc;
}
/**
* Shared state
*/
get ystate(): Y.Map<any> {
return this._ystate;
}
/**
* YJS document undo manager
*/
get undoManager(): Y.UndoManager {
return this._undoManager;
}
/**
* Shared awareness
*/
get awareness(): Awareness {
return this._awareness;
}
/**
* The changed signal.
*/
get changed(): ISignal<this, T> {
return this._changed;
}
/**
* A signal emitted when the document is disposed.
*/
get disposed(): ISignal<this, void> {
return this._disposed;
}
/**
* Whether the document is disposed or not.
*/
get isDisposed(): boolean {
return this._isDisposed;
}
/**
* Document state
*/
get state(): JSONObject {
return JSONExt.deepCopy(this.ystate.toJSON());
}
/**
* Whether the object can undo changes.
*/
canUndo(): boolean {
return this.undoManager.undoStack.length > 0;
}
/**
* Whether the object can redo changes.
*/
canRedo(): boolean {
return this.undoManager.redoStack.length > 0;
}
/**
* Dispose of the resources.
*/
dispose(): void {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
this.ystate.unobserve(this.onStateChanged);
this.awareness.destroy();
this.undoManager.destroy();
this.ydoc.destroy();
this._disposed.emit();
Signal.clearData(this);
}
/**
* Get the value for a state attribute
*
* @param key Key to get
*/
getState(key: string): JSONValue | undefined {
const value = this.ystate.get(key);
return typeof value === 'undefined'
? value
: (JSONExt.deepCopy(value) as unknown as JSONValue);
}
/**
* Set the value of a state attribute
*
* @param key Key to set
* @param value New attribute value
*/
setState(key: string, value: JSONValue): void {
if (!JSONExt.deepEqual(this.ystate.get(key), value)) {
this.ystate.set(key, value);
}
}
/**
* Get the document source
*
* @returns The source
*/
get source(): JSONValue | string {
return this.getSource();
}
/**
* Set the document source
*
* @param value The source to set
*/
set source(value: JSONValue | string) {
this.setSource(value);
}
/**
* Get the document source
*
* @returns The source
*/
abstract getSource(): JSONValue | string;
/**
* Set the document source
*
* @param value The source to set
*/
abstract setSource(value: JSONValue | string): void;
/**
* Undo an operation.
*/
undo(): void {
this.undoManager.undo();
}
/**
* Redo an operation.
*/
redo(): void {
this.undoManager.redo();
}
/**
* Clear the change stack.
*/
clearUndoHistory(): void {
this.undoManager.clear();
}
/**
* Perform a transaction. While the function f is called, all changes to the shared
* document are bundled into a single event.
*/
transact(f: () => void, undoable = true, origin: any = null): void {
this.ydoc.transact(f, undoable ? this : origin);
}
/**
* Handle a change to the ystate.
*/
protected onStateChanged = (event: Y.YMapEvent<any>): void => {
const stateChange = new Array<StateChange<any>>();
event.keysChanged.forEach(key => {
const change = event.changes.keys.get(key);
if (change) {
stateChange.push({
name: key,
oldValue: change.oldValue,
newValue: this.ystate.get(key)
});
}
});
this._changed.emit({ stateChange } as any);
};
protected _changed = new Signal<this, T>(this);
private _ydoc: Y.Doc;
private _ystate: Y.Map<any>;
private _undoManager: Y.UndoManager;
private _awareness: Awareness;
private _isDisposed = false;
private _disposed = new Signal<this, void>(this);
}
/**
* YDocument namespace
*/
export namespace YDocument {
/**
* YDocument constructor options
*/
export interface IOptions {
/**
* The optional YJS document for YDocument.
*/
ydoc?: Y.Doc;
}
}