Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify Snapshot Event to publish updated local changes #923

Merged
merged 10 commits into from
Nov 7, 2024
51 changes: 44 additions & 7 deletions examples/vanilla-codemirror6/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/* eslint-disable jsdoc/require-jsdoc */
import yorkie, { DocEventType } from 'yorkie-js-sdk';
import type { TextOperationInfo, EditOpInfo } from 'yorkie-js-sdk';
import type { EditOpInfo, OperationInfo } from 'yorkie-js-sdk';
import { basicSetup, EditorView } from 'codemirror';
import { keymap } from '@codemirror/view';
import {
markdown,
markdownKeymap,
markdownLanguage,
} from '@codemirror/lang-markdown';
import { Transaction } from '@codemirror/state';
import { Transaction, TransactionSpec } from '@codemirror/state';
import { network } from './network';
import { displayLog, displayPeers } from './utils';
import { YorkieDoc } from './type';
import { YorkieDoc, YorkiePresence } from './type';
import './style.css';

const editorParentElem = document.getElementById('editor')!;
Expand All @@ -28,7 +28,7 @@ async function main() {
await client.activate();

// 02-1. create a document then attach it into the client.
const doc = new yorkie.Document<YorkieDoc>(
const doc = new yorkie.Document<YorkieDoc, YorkiePresence>(
`codemirror6-${new Date()
.toISOString()
.substring(0, 10)
Expand All @@ -55,10 +55,21 @@ async function main() {
// 02-2. subscribe document event.
const syncText = () => {
const text = doc.getRoot().content;
view.dispatch({
const selection = doc.getMyPresence().selection;
const transactionSpec: TransactionSpec = {
changes: { from: 0, to: view.state.doc.length, insert: text.toString() },
annotations: [Transaction.remote.of(true)],
});
};

if (selection) {
// Restore the cursor position when the text is replaced.
const cursor = text.posRangeToIndexRange(selection);
transactionSpec['selection'] = {
anchor: cursor[0],
head: cursor[1],
};
}
view.dispatch(transactionSpec);
};
doc.subscribe((event) => {
if (event.type === 'snapshot') {
Expand Down Expand Up @@ -98,6 +109,32 @@ async function main() {
});
}
}

const hasFocus =
viewUpdate.view.hasFocus && viewUpdate.view.dom.ownerDocument.hasFocus();
const sel = hasFocus ? viewUpdate.state.selection.main : null;

doc.update((root, presence) => {
if (sel && root.content) {
const selection = root.content.indexRangeToPosRange([
sel.anchor,
sel.head,
]);

if (
JSON.stringify(selection) !==
JSON.stringify(presence.get('selection'))
) {
presence.set({
selection,
});
}
} else if (presence.get('selection')) {
presence.set({
selection: undefined,
});
}
});
});

// 03-2. create codemirror instance
Expand All @@ -113,7 +150,7 @@ async function main() {
});

// 03-3. define event handler that apply remote changes to local
function handleOperations(operations: Array<TextOperationInfo>) {
function handleOperations(operations: Array<OperationInfo>) {
for (const op of operations) {
if (op.type === 'edit') {
handleEditOp(op);
Expand Down
6 changes: 5 additions & 1 deletion examples/vanilla-codemirror6/src/type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { type Text } from 'yorkie-js-sdk';
import { TextPosStructRange, type Text } from 'yorkie-js-sdk';

export type YorkieDoc = {
content: Text;
};

export type YorkiePresence = {
selection?: TextPosStructRange;
};
187 changes: 99 additions & 88 deletions packages/sdk/src/document/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,14 +470,14 @@ export type DocumentKey = string;
type OperationInfoOfElement<TElement> = TElement extends Text
? TextOperationInfo
: TElement extends Counter
? CounterOperationInfo
: TElement extends Tree
? TreeOperationInfo
: TElement extends BaseArray<any>
? ArrayOperationInfo
: TElement extends BaseObject<any>
? ObjectOperationInfo
: OperationInfo;
? CounterOperationInfo
: TElement extends Tree
? TreeOperationInfo
: TElement extends BaseArray<any>
? ArrayOperationInfo
: TElement extends BaseObject<any>
? ObjectOperationInfo
: OperationInfo;

/**
* `OperationInfoOfInternal` represents the type of the operation info of the
Expand All @@ -498,49 +498,49 @@ type OperationInfoOfInternal<
> = TDepth extends 0
? TElement
: TKeyOrPath extends `${infer TFirst}.${infer TRest}`
? TFirst extends keyof TElement
? TElement[TFirst] extends BaseArray<unknown>
? OperationInfoOfInternal<
TElement[TFirst],
number,
DecreasedDepthOf<TDepth>
>
: OperationInfoOfInternal<
TElement[TFirst],
TRest,
DecreasedDepthOf<TDepth>
>
: OperationInfo
: TKeyOrPath extends keyof TElement
? TElement[TKeyOrPath] extends BaseArray<unknown>
? ArrayOperationInfo
: OperationInfoOfElement<TElement[TKeyOrPath]>
: OperationInfo;
? TFirst extends keyof TElement
? TElement[TFirst] extends BaseArray<unknown>
? OperationInfoOfInternal<
TElement[TFirst],
number,
DecreasedDepthOf<TDepth>
>
: OperationInfoOfInternal<
TElement[TFirst],
TRest,
DecreasedDepthOf<TDepth>
>
: OperationInfo
: TKeyOrPath extends keyof TElement
? TElement[TKeyOrPath] extends BaseArray<unknown>
? ArrayOperationInfo
: OperationInfoOfElement<TElement[TKeyOrPath]>
: OperationInfo;

/**
* `DecreasedDepthOf` represents the type of the decreased depth of the given depth.
*/
type DecreasedDepthOf<Depth extends number = 0> = Depth extends 10
? 9
: Depth extends 9
? 8
: Depth extends 8
? 7
: Depth extends 7
? 6
: Depth extends 6
? 5
: Depth extends 5
? 4
: Depth extends 4
? 3
: Depth extends 3
? 2
: Depth extends 2
? 1
: Depth extends 1
? 0
: -1;
? 8
: Depth extends 8
? 7
: Depth extends 7
? 6
: Depth extends 6
? 5
: Depth extends 5
? 4
: Depth extends 4
? 3
: Depth extends 3
? 2
: Depth extends 2
? 1
: Depth extends 1
? 0
: -1;

/**
* `PathOfInternal` represents the type of the path of the given element.
Expand All @@ -552,29 +552,29 @@ type PathOfInternal<
> = Depth extends 0
? Prefix
: TElement extends Record<string, any>
? {
[TKey in keyof TElement]: TElement[TKey] extends LeafElement
? `${Prefix}${TKey & string}`
: TElement[TKey] extends BaseArray<infer TArrayElement>
?
| `${Prefix}${TKey & string}`
| `${Prefix}${TKey & string}.${number}`
| PathOfInternal<
TArrayElement,
`${Prefix}${TKey & string}.${number}.`,
DecreasedDepthOf<Depth>
>
:
| `${Prefix}${TKey & string}`
| PathOfInternal<
TElement[TKey],
`${Prefix}${TKey & string}.`,
DecreasedDepthOf<Depth>
>;
}[keyof TElement]
: Prefix extends `${infer TRest}.`
? TRest
: Prefix;
? {
[TKey in keyof TElement]: TElement[TKey] extends LeafElement
? `${Prefix}${TKey & string}`
: TElement[TKey] extends BaseArray<infer TArrayElement>
?
| `${Prefix}${TKey & string}`
| `${Prefix}${TKey & string}.${number}`
| PathOfInternal<
TArrayElement,
`${Prefix}${TKey & string}.${number}.`,
DecreasedDepthOf<Depth>
>
:
| `${Prefix}${TKey & string}`
| PathOfInternal<
TElement[TKey],
`${Prefix}${TKey & string}.`,
DecreasedDepthOf<Depth>
>;
}[keyof TElement]
: Prefix extends `${infer TRest}.`
? TRest
: Prefix;

/**
* `OperationInfoOf` represents the type of the operation info of the given
Expand Down Expand Up @@ -1148,6 +1148,22 @@ export class Document<T, P extends Indexable = Indexable> {
return targetPath.every((path, index) => path === nodePath[index]);
}

/**
* `removePushedLocalChanges` removes local changes that have been applied to
* the server from the local changes.
*
* @param clientSeq - client sequence number to remove local changes before it
*/
private removePushedLocalChanges(clientSeq: number) {
while (this.localChanges.length) {
const change = this.localChanges[0];
if (change.getID().getClientSeq() > clientSeq) {
break;
}
this.localChanges.shift();
}
}

/**
* `applyChangePack` applies the given change pack into this document.
* 1. Remove local changes applied to server.
Expand All @@ -1160,42 +1176,28 @@ export class Document<T, P extends Indexable = Indexable> {
public applyChangePack(pack: ChangePack<P>): void {
const hasSnapshot = pack.hasSnapshot();

// 01. Apply snapshot or changes to the root object.
if (hasSnapshot) {
this.applySnapshot(
pack.getCheckpoint().getServerSeq(),
pack.getVersionVector()!,
pack.getSnapshot()!,
pack.getCheckpoint().getClientSeq(),
);
} else if (pack.hasChanges()) {
} else {
this.applyChanges(pack.getChanges(), OpSource.Remote);
this.removePushedLocalChanges(pack.getCheckpoint().getClientSeq());
}

// 02. Remove local changes applied to server.
while (this.localChanges.length) {
const change = this.localChanges[0];
if (change.getID().getClientSeq() > pack.getCheckpoint().getClientSeq()) {
break;
}
this.localChanges.shift();
}

// NOTE(hackerwins): If the document has local changes, we need to apply
// them after applying the snapshot. We need to treat the local changes
// as remote changes because the application should apply the local
// changes to their own document.
if (hasSnapshot) {
this.applyChanges(this.localChanges, OpSource.Remote);
}

// 03. Update the checkpoint.
// 02. Update the checkpoint.
this.checkpoint = this.checkpoint.forward(pack.getCheckpoint());

// 04. Do Garbage collection.
// 03. Do Garbage collection.
if (!hasSnapshot) {
this.garbageCollect(pack.getVersionVector()!);
}

// 05. Filter detached client's lamport from version vector
// 04. Filter detached client's lamport from version vector
if (!hasSnapshot) {
this.filterVersionVector(pack.getVersionVector()!);
}
Expand Down Expand Up @@ -1412,6 +1414,7 @@ export class Document<T, P extends Indexable = Indexable> {
serverSeq: bigint,
snapshotVector: VersionVector,
snapshot?: Uint8Array,
clientSeq: number = -1,
) {
const { root, presences } = converter.bytesToSnapshot<P>(snapshot);
this.root = new CRDTRoot(root);
Expand All @@ -1421,6 +1424,13 @@ export class Document<T, P extends Indexable = Indexable> {
// drop clone because it is contaminated.
this.clone = undefined;

this.removePushedLocalChanges(clientSeq);

// NOTE(hackerwins): If the document has local changes, we need to apply
// them after applying the snapshot, as local changes are not included in the snapshot data.
// Afterward, we should publish a snapshot event with the latest
// version of the document to ensure the user receives the most up-to-date snapshot.
this.applyChanges(this.localChanges, OpSource.Local);
this.publish([
{
type: DocEventType.Snapshot,
Expand Down Expand Up @@ -1689,6 +1699,7 @@ export class Document<T, P extends Indexable = Indexable> {
if (event.type === DocEventType.Snapshot) {
const { snapshot, serverSeq, snapshotVector } = event.value;
if (!snapshot) return;
// TODO(hackerwins): We need to check the clientSeq of the snapshot.
this.applySnapshot(
BigInt(serverSeq),
converter.hexToVersionVector(snapshotVector),
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/test/helper/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { InitialActorID } from '@yorkie-js-sdk/src/document/time/actor_id';
import { VersionVector } from '@yorkie-js-sdk/src/document/time/version_vector';

export type Indexable = Record<string, any>;
export const DefaultSnapshotThreshold = 1000;

/**
* EventCollector provides a utility to collect and manage events.
Expand Down
Loading
Loading