8000 GitHub - skyterra/y-crdt
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

skyterra/y-crdt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

y-crdt

A Golang implementation of the Yjs algorithms, designed to serve as a robust backend server for multi-terminal document collaboration. This implementation enhances real-time collaboration experiences across diverse user scenarios by efficiently merging updates from various terminals, extracting differential data, and supporting data archiving.

In the future, we plan to develop a complete y-server service that synchronizes data with client terminals via WebSocket. Stay tuned for updates!

Compatibility test

Test cases are implemented in compatibility_test.go , focusing on validating cross-version and cross-language compatibility with Yjs.
Note: Encoder/decoder v2 support is pending development.

Compatibility testing passed.

package y_crdt

import (
	"bytes"
	"encoding/json"
	"testing"

	"github.com/bytedance/mockey"
)

func TestTextInsertDelete(t *testing.T) {
	// Generated via:
	//     ```js
	//        const doc = new Y.Doc()
	//        const ytext = doc.getText('type')
	//        doc..transact_mut()(function () {
	//            ytext.insert(0, 'def')
	//            ytext.insert(0, 'abc')
	//            ytext.insert(6, 'ghi')
	//            ytext.delete(2, 5)
	//        })
	//        const update = Y.encodeStateAsUpdate(doc)
	//        ytext.toString() // => 'abhi'
	//     ```
	//
	//     This way we confirm that we can decode and apply:
	//     1. blocks without left/right origin consisting of multiple characters
	//     2. blocks with left/right origin consisting of multiple characters
	//     3. delete sets

	// construct doc by golang and check to see if the result is the same as the expected.
	doc := NewDoc("guid", false, nil, nil, false)
	ytext := doc.GetText("type")
	doc.Transact(func(trans *Transaction) {
		ytext.Insert(0, "def", nil)
		ytext.Insert(0, "abc", nil)
		ytext.Insert(6, "ghi", nil)
		ytext.Delete(2, 5)
	}, nil)

	if ytext.ToString() != "abhi" {
		t.Error("expected abhi, got ", ytext.ToString())
	}
	t.Logf("construct by golang, ytext is %s", ytext.ToString())

	// the payload was generated by javascript.
	var payload = []byte{
		1, 5, 152, 234, 173, 126, 0, 1, 1, 4, 116, 121, 112, 101, 3, 68, 152, 234, 173, 126, 0, 2,
		97, 98, 193, 152, 234, 173, 126, 4, 152, 234, 173, 126, 0, 1, 129, 152, 234, 173, 126, 2,
		1, 132, 152, 234, 173, 126, 6, 2, 104, 105, 1, 152, 234, 173, 126, 2, 0, 3, 5, 2,
	}

	// apply the update and check to see if the result is the same as the expected.
	doc = NewDoc("guid", false, nil, nil, false)
	doc.Transact(func(trans *Transaction) {
		ApplyUpdate(doc, payload, nil)
	}, nil)

	ytext = doc.GetText("type")
	if ytext.ToString() != "abhi" {
		t.Errorf("expected abhi, got %s", ytext.ToString())
	}
	t.Logf("after apply update, ytext is %s", ytext.ToString())
}

func TestMapSet(t *testing.T) {
	//  Generated via:
	//     ```js
	//        const doc = new Y.Doc()
	//        const x = doc.getMap('test')
	//        x.set('k1', 'v1')
	//        x.set('k2', 'v2')
	//        const payload_v1 = Y.encodeStateAsUpdate(doc)
	//        console.log(payload_v1);
	//        const payload_v2 = Y.encodeStateAsUpdateV2(doc)
	//        console.log(payload_v2);
	//     ```

	mocker := mockey.Mock(GenerateNewClientID).Return(440166001).Build()

	// construct doc by golang and check to see if the result is the same as the expected.
	doc := NewDoc("guid", false, nil, nil, false)
	x := doc.GetMap("test").(*YMap)
	doc.Transact(func(trans *Transaction) {
		x.Set("k1", "v1")
		x.Set("k2", "v2")
	}, nil)

	content, err := json.Marshal(x.ToJson())
	if err != nil {
		t.Errorf("marshal x to json, err is %v", err)
	}

	t.Logf("construct by golang, x is %s", content)

	m := make(map[string]string)
	err = json.Unmarshal(content, &m)
	if err != nil || len(m) != 2 || m["k1"] != "v1" || m["k2"] != "v2" {
		t.Errorf("expected {\"k1\":\"v1\",\"k2\":\"v2\"}, got %s", content)
	}

	// the payload was generated by javascript.
	var payload = []byte{
		1, 2, 241, 204, 241, 209, 1, 0, 40, 1, 4, 116, 101, 115, 116, 2, 107, 49, 1, 119, 2, 118,
		49, 40, 1, 4, 116, 101, 115, 116, 2, 107, 50, 1, 119, 2, 118, 50, 0,
	}

	// encode doc(geneareted by golang) and compare with payload(generated by javascript).
	update := EncodeStateAsUpdate(doc, nil)
	t.Logf("update is %v", update)
	if !bytes.Equal(update, payload) {
		t.Errorf("expect update:%v got update:%v", payload, update)
	}

	// apply the update(v1) and check to see if the result is the same as the expected.
	mocker.UnPatch()
	doc = NewDoc("guid", false, nil, nil, false)
	doc.Transact(func(trans *Transaction) {
		ApplyUpdate(doc, payload, nil)
	}, nil)

	content, err = json.Marshal(doc.GetMap("test").ToJson())
	if err != nil {
		t.Errorf("marshal doc.GetMap(\"test\") to json, err is %v", err)
	}

	t.Logf("after apply update, x is %s", content)

	m = make(map[string]string)
	err = json.Unmarshal(content, &m)
	if err != nil || len(m) != 2 || m["k1"] != "v1" || m["k2"] != "v2" {
		t.Errorf("expected {\"k1\":\"v1\",\"k2\":\"v2\"}, got %s", content)
	}

	// decoder v2 not support yet.
}

func TestArrayInsert(t *testing.T) {
	// Generated via:
	//     ```js
	//        const doc = new Y.Doc()
	//        const x = doc.getArray('test')
	//        x.push(['a']);
	//        x.push(['b']);
	//        const payload_v1 = Y.encodeStateAsUpdate(doc)
	//        console.log(payload_v1);
	//        const payload_v2 = Y.encodeStateAsUpdateV2(doc)
	//        console.log(payload_v2);
	//     ```

	mocker := mockey.Mock(GenerateNewClientID).Return(2525665872).Build()

	// construct doc by golang and check to see if the result is the same as the expected.
	doc := NewDoc("guid", false, nil, nil, false)
	x := doc.GetArray("test")
	x.Push([]any{"a"})
	x.Push([]any{"b"})

	content, err := json.Marshal(x.ToJson())
	t.Logf("construct by golang, x is %s, err is %v", content, err)

	if !bytes.Equal(content, []byte("[\"a\",\"b\"]")) {
		t.Errorf("expected [\"a\",\"b\"], got %s", content)
	}

	// the payload was generated by javascript.
	var payload = []byte{
		1, 1, 
2E87
208, 180, 170, 180, 9, 0, 8, 1, 4, 116, 101, 115, 116, 2, 119, 1, 97, 119, 1, 98, 0,
	}

	// encode doc(geneareted by golang) and compare with payload(generated by javascript).
	update := EncodeStateAsUpdate(doc, nil)
	t.Logf("update is %v", update)
	if !bytes.Equal(update, payload) {
		t.Errorf("expect update:%v got update:%v", payload, update)
	}

	// apply the update(v1) and check to see if the result is the same as the expected.
	mocker.UnPatch()
	doc = NewDoc("new doc", false, nil, nil, false)
	ApplyUpdate(doc, payload, nil)

	content, err = json.Marshal(doc.GetArray("test").ToJson())
	t.Logf("after apply update, x is %s, err is %v", content, err)

	if !bytes.Equal(content, []byte("[\"a\",\"b\"]")) {
		t.Errorf("expected [\"a\",\"b\"], got %s", content)
	}
}

func TestXmlFragmentInsert(t *testing.T) {
	//  Generated via:
	// ```js
	//    const ydoc = new Y.Doc()
	//    const yxmlFragment = ydoc.getXmlFragment('fragment-name')
	//    const yxmlNested = new Y.XmlFragment('fragment-name')
	//    const yxmlText = new Y.XmlText()
	//    yxmlFragment.insert(0, [yxmlText])
	//    yxmlFragment.firstChild === yxmlText
	//    yxmlFragment.insertAfter(yxmlText, [new Y.XmlElement('node-name')])
	//    const payload_v1 = Y.encodeStateAsUpdate(ydoc)
	//    console.log(payload_v1);
	//    const payload_v2 = Y.encodeStateAsUpdateV2(ydoc)
	//    console.log(payload_v2);
	// ```

	mockey.Mock(GenerateNewClientID).Return(2459881872).Build()

	// construct doc by golang and check to see if the result is the same as the expected.
	doc := NewDoc("guid", false, nil, nil, false)
	yxmlFragment := doc.GetXmlFragment("fragment-name").(*YXmlFragment)
	yxmlText := NewYXmlText()
	yxmlFragment.Insert(0, ArrayAny{yxmlText})

	if yxmlFragment.GetFirstChild().(*YXmlText) != yxmlText {
		t.Errorf("expected yxmlFragment.GetFirstChild() is yxmlText, got %v", yxmlFragment.GetFirstChild())
	}

	yxmlFragment.InsertAfter(yxmlText, ArrayAny{NewYXmlElement("node-name")})
	update := EncodeStateAsUpdate(doc, nil)

	var payload = []byte{
		1, 2, 144, 163, 251, 148, 9, 0, 7, 1, 13, 102, 114, 97, 103, 109, 101, 110, 116, 45, 110,
		97, 109, 101, 6, 135, 144, 163, 251, 148, 9, 0, 3, 9, 110, 111, 100, 101, 45, 110, 97, 109,
		101, 0,
	}

	if !bytes.Equal(update, payload) {
		t.Errorf("expected update:%v got update:%v", payload, update)
	}
}

func TestStateVector(t *testing.T) {
	//  Generated via:
	//   ```js
	//      const a = new Y.Doc()
	//      const ta = a.getText('test')
	//      ta.insert(0, 'abc')

	//      const b = new Y.Doc()
	//      const tb = b.getText('test')
	//      tb.insert(0, 'de')

	//      Y.applyUpdate(a, Y.encodeStateAsUpdate(b))
	//      console.log(Y.encodeStateVector(a))
	//   ```

	var payload = []byte{2, 178, 219, 218, 44, 3, 190, 212, 225, 6, 2}
	sv := DecodeStateVector(payload)

	expected := map[Number]Number{
		14182974: 2,
		93760946: 3,
	}

	for k, v := range expected {
		if sv[k] != v {
			t.Errorf("expected %v, got %v", v, sv[k])
		}
	}

	serialized := EncodeStateVector(nil, sv, NewUpdateEncoderV1())
	if !bytes.Equal(serialized, payload) {
		t.Errorf("expected: %v, got: %v", payload, serialized)
	}
}

Encoding & Decoding

Encoding

Basic Types

  • WriteByte: Write uint8 to buffer.
  • WriteVarUint: Variable-length uint64 encoding.
  • WriteVarInt: Variable-length int encoding with sign handling.
  • WriteFloat32/64: Big-endian float encoding.
  • WriteInt64: Big-endian int64 encoding.

Composite Types

  • WriteString: Write length + string data.
  • WriteObject: Write key-value count, then key-value pairs.
  • WriteArray: Write element count, then elements.
  • WriteVarUint8Array/WriteUint8Array: Write byte arrays with/without length prefix.

Universal

  • WriteAny: Type-specific encoding with flag bytes.

Decoding

Basic Types

  • readVarUint: Read variable-length uint64.
  • ReadUint8: Read uint8.
  • ReadVarInt: Restore int from variable-length bytes.
  • ReadFloat32/64: Parse big-endian floats.
  • ReadBigInt64: Parse big-endian int64.

Composite Types

  • ReadString: Read length, then string data.
  • ReadObject: Read count, then key-value pairs.
  • ReadArray: Read count, then elements.
  • ReadVarUint8Array: Read length, then byte array.

Universal

  • ReadAny: Decode based on type flag byte.

Encoding and decoding are symmetric, using variable-length and big-endian formats for efficiency.

Yjs Data Structures

YMap

  • YMap: A key-value store with efficient updates.
  • YMapItem: Represents a key-value pair in the map.
  • YMapItemMap: Maps keys to YMapItem pointers.

YArray

  • YArray: A dynamic array with efficient updates.
  • YArrayItem: Represents an element in the array.

YText

  • YText: A text document with efficient updates.
  • YTextItem: Represents a character in the text.
  • YTextItemMap: Maps positions to YTextItem pointers.

YXmlFragment

  • YXmlFragment: A fragment of an XML document.
  • YXmlFragmentItem: Represents an XML node.
  • YXmlFragmentItemMap: Maps positions to YXmlFragmentItem pointers.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published
0