Skip to content

Commit

Permalink
Update protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
MoonGyu1 committed Sep 15, 2023
1 parent 940941a commit f14c165
Show file tree
Hide file tree
Showing 10 changed files with 691 additions and 225 deletions.
32 changes: 30 additions & 2 deletions api/converter/from_pb.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,11 +432,11 @@ func fromStyle(pbStyle *api.Operation_Style) (*operations.Style, error) {
if err != nil {
return nil, err
}
from, err := fromTextNodePos(pbStyle.From)
from, err := fromTextNodeBoundary(pbStyle.From)
if err != nil {
return nil, err
}
to, err := fromTextNodePos(pbStyle.To)
to, err := fromTextNodeBoundary(pbStyle.To)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -580,6 +580,34 @@ func fromTextNodePos(
), nil
}

func fromTextNodeBoundary(
pbBoundary *api.TextNodeBoundary,
) (*crdt.RGATreeSplitNodeBoundary, error) {
createdAt, err := fromTimeTicket(pbBoundary.CreatedAt)
if err != nil {
return nil, err
}

var boundaryType crdt.BoundaryType
switch pbType := pbBoundary.Type; pbType {
case api.BoundaryType_BOUNDARY_TYPE_BEFORE:
boundaryType = crdt.Before
case api.BoundaryType_BOUNDARY_TYPE_AFTER:
boundaryType = crdt.After
case api.BoundaryType_BOUNDARY_TYPE_START:
boundaryType = crdt.Start
case api.BoundaryType_BOUNDARY_TYPE_END:
boundaryType = crdt.End
default:
boundaryType = ""
}

return crdt.NewRGATreeSplitNodeBoundary(
crdt.NewRGATreeSplitNodeID(createdAt, int(pbBoundary.Offset)),
boundaryType,
), nil
}

// FromTreeNodes converts protobuf tree nodes to crdt.TreeNode. The last node
// in the slice is the root node, because the slice is in post-order.
func FromTreeNodes(pbNodes []*api.TreeNode) (*crdt.TreeNode, error) {
Expand Down
38 changes: 36 additions & 2 deletions api/converter/to_pb.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,8 +355,8 @@ func toStyle(style *operations.Style) (*api.Operation_Style_, error) {
return &api.Operation_Style_{
Style: &api.Operation_Style{
ParentCreatedAt: ToTimeTicket(style.ParentCreatedAt()),
From: toTextNodePos(style.From()),
To: toTextNodePos(style.To()),
From: toTextNodeBoundary(style.From()),
To: toTextNodeBoundary(style.To()),
CreatedAtMapByActor: toCreatedAtMapByActor(style.CreatedAtMapByActor()),
Attributes: style.Attributes(),
ExecutedAt: ToTimeTicket(style.ExecutedAt()),
Expand Down Expand Up @@ -470,6 +470,40 @@ func toTextNodePos(pos *crdt.RGATreeSplitNodePos) *api.TextNodePos {
}
}

func toTextNodeBoundary(boundary *crdt.RGATreeSplitNodeBoundary) *api.TextNodeBoundary {
switch boundary.Type() {
case crdt.Before:
return &api.TextNodeBoundary{
CreatedAt: ToTimeTicket(boundary.ID().CreatedAt()),
Offset: int32(boundary.ID().Offset()),
Type: api.BoundaryType_BOUNDARY_TYPE_BEFORE,
}
case crdt.After:
return &api.TextNodeBoundary{
CreatedAt: ToTimeTicket(boundary.ID().CreatedAt()),
Offset: int32(boundary.ID().Offset()),
Type: api.BoundaryType_BOUNDARY_TYPE_AFTER,
}
case crdt.Start:
return &api.TextNodeBoundary{
CreatedAt: ToTimeTicket(boundary.ID().CreatedAt()),
Offset: int32(boundary.ID().Offset()),
Type: api.BoundaryType_BOUNDARY_TYPE_START,
}
case crdt.End:
return &api.TextNodeBoundary{
CreatedAt: ToTimeTicket(boundary.ID().CreatedAt()),
Offset: int32(boundary.ID().Offset()),
Type: api.BoundaryType_BOUNDARY_TYPE_END,
}
default:
return &api.TextNodeBoundary{
CreatedAt: ToTimeTicket(boundary.ID().CreatedAt()),
Offset: int32(boundary.ID().Offset()),
}
}
}

func toCreatedAtMapByActor(
createdAtMapByActor map[string]*time.Ticket,
) map[string]*api.TimeTicket {
Expand Down
648 changes: 473 additions & 175 deletions api/yorkie/v1/resources.pb.go

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions api/yorkie/v1/resources.proto
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ message Operation {
}
message Style {
TimeTicket parent_created_at = 1;
TextNodePos from = 2;
TextNodePos to = 3;
TextNodeBoundary from = 2;
TextNodeBoundary to = 3;
map<string, string> attributes = 4;
TimeTicket executed_at = 5;
map<string, TimeTicket> created_at_map_by_actor = 6;
Expand Down Expand Up @@ -331,6 +331,19 @@ message TextNodePos {
int32 relative_offset = 3;
}

enum BoundaryType {
BOUNDARY_TYPE_BEFORE = 0;
BOUNDARY_TYPE_AFTER = 1;
BOUNDARY_TYPE_START = 2;
BOUNDARY_TYPE_END = 3;
}

message TextNodeBoundary {
TimeTicket created_at = 1;
int32 offset = 2;
BoundaryType type = 3;
}

message TimeTicket {
int64 lamport = 1 [jstype = JS_STRING];
uint32 delimiter = 2;
Expand Down
53 changes: 53 additions & 0 deletions pkg/document/crdt/rga_tree_split.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ var (
initialNodeID = NewRGATreeSplitNodeID(time.InitialTicket, 0)
)

// BoundaryType represents any type that can be used as a boundary.
type BoundaryType string

// The values below are the types that can be used as boundary.
const (
Before BoundaryType = "before"
After BoundaryType = "after"
Start BoundaryType = "start"
End BoundaryType = "end"
)

// RGATreeSplitValue is a value of RGATreeSplitNode.
type RGATreeSplitValue interface {
Split(offset int) RGATreeSplitValue
Expand Down Expand Up @@ -144,6 +155,27 @@ func (pos *RGATreeSplitNodePos) Equal(other *RGATreeSplitNodePos) bool {
return pos.relativeOffset == other.relativeOffset
}

// RGATreeSplitNodeBoundary is a boundary of RGATreeSplitNode.
type RGATreeSplitNodeBoundary struct {
id *RGATreeSplitNodeID
boundaryType BoundaryType
}

// NewRGATreeSplitNodeBoundary creates a new instance of NewRGATreeSplitNodeBoundary.
func NewRGATreeSplitNodeBoundary(id *RGATreeSplitNodeID, boundaryType BoundaryType) *RGATreeSplitNodeBoundary {
return &RGATreeSplitNodeBoundary{id, boundaryType}
}

// ID returns the ID of this RGATreeSplitNodeBoundary.
func (boundary *RGATreeSplitNodeBoundary) ID() *RGATreeSplitNodeID {
return boundary.id
}

// Type returns the boundary type of this RGATreeSplitNodeBoundary.
func (boundary *RGATreeSplitNodeBoundary) Type() BoundaryType {
return boundary.boundaryType
}

// RGATreeSplitNode is a node of RGATreeSplit.
type RGATreeSplitNode[V RGATreeSplitValue] struct {
id *RGATreeSplitNodeID
Expand Down Expand Up @@ -354,6 +386,27 @@ func (s *RGATreeSplit[V]) findNodeWithSplit(
return node, node.next, nil
}

func (s *RGATreeSplit[V]) splitNodeByBoundary(
boundary *RGATreeSplitNodeBoundary,
) error {
absoluteID := boundary.ID()
if absoluteID.CreatedAt() != nil {
node, err := s.findFloorNodePreferToLeft(absoluteID)
if err != nil {
return err
}

relativeOffset := absoluteID.offset - node.id.offset

_, err = s.splitNode(node, relativeOffset)
if err != nil {
return err
}
}

return nil
}

func (s *RGATreeSplit[V]) findFloorNodePreferToLeft(id *RGATreeSplitNodeID) (*RGATreeSplitNode[V], error) {
node := s.findFloorNode(id)
if node == nil {
Expand Down
16 changes: 8 additions & 8 deletions pkg/document/crdt/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,28 +63,28 @@ func TestRoot(t *testing.T) {
ctx := helper.TextChangeContext(root)
text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket())

fromPos, toPos, _ := text.CreateRange(0, 0)
fromPos, toPos, _ := text.CreatePosRange(0, 0)
_, _, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
registerElementHasRemovedNodes(fromPos, toPos, root, text)
assert.Equal(t, "Hello World", text.String())
assert.Equal(t, 0, root.GarbageLen())

fromPos, toPos, _ = text.CreateRange(5, 10)
fromPos, toPos, _ = text.CreatePosRange(5, 10)
_, _, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
registerElementHasRemovedNodes(fromPos, toPos, root, text)
assert.Equal(t, "HelloYorkied", text.String())
assert.Equal(t, 1, root.GarbageLen())

fromPos, toPos, _ = text.CreateRange(0, 5)
fromPos, toPos, _ = text.CreatePosRange(0, 5)
_, _, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
registerElementHasRemovedNodes(fromPos, toPos, root, text)
assert.Equal(t, "Yorkied", text.String())
assert.Equal(t, 2, root.GarbageLen())

fromPos, toPos, _ = text.CreateRange(6, 7)
fromPos, toPos, _ = text.CreatePosRange(6, 7)
_, _, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
registerElementHasRemovedNodes(fromPos, toPos, root, text)
Expand Down Expand Up @@ -125,7 +125,7 @@ func TestRoot(t *testing.T) {
}

for _, tc := range tests {
fromPos, toPos, _ := text.CreateRange(tc.from, tc.to)
fromPos, toPos, _ := text.CreatePosRange(tc.from, tc.to)
_, _, err := text.Edit(fromPos, toPos, nil, tc.content, nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
registerElementHasRemovedNodes(fromPos, toPos, root, text)
Expand All @@ -144,21 +144,21 @@ func TestRoot(t *testing.T) {
ctx := helper.TextChangeContext(root)
text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket())

fromPos, toPos, _ := text.CreateRange(0, 0)
fromPos, toPos, _ := text.CreatePosRange(0, 0)
_, _, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
registerElementHasRemovedNodes(fromPos, toPos, root, text)
assert.Equal(t, `[{"val":"Hello World"}]`, text.Marshal())
assert.Equal(t, 0, root.GarbageLen())

fromPos, toPos, _ = text.CreateRange(6, 11)
fromPos, toPos, _ = text.CreatePosRange(6, 11)
_, _, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
registerElementHasRemovedNodes(fromPos, toPos, root, text)
assert.Equal(t, `[{"val":"Hello "},{"val":"Yorkie"}]`, text.Marshal())
assert.Equal(t, 1, root.GarbageLen())

fromPos, toPos, _ = text.CreateRange(0, 6)
fromPos, toPos, _ = text.CreatePosRange(0, 6)
_, _, err = text.Edit(fromPos, toPos, nil, "", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
registerElementHasRemovedNodes(fromPos, toPos, root, text)
Expand Down
51 changes: 39 additions & 12 deletions pkg/document/crdt/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,24 @@ func (t *Text) Remove(removedAt *time.Ticket) bool {
return false
}

// CreateRange returns a pair of RGATreeSplitNodePos of the given integer offsets.
func (t *Text) CreateRange(from, to int) (*RGATreeSplitNodePos, *RGATreeSplitNodePos, error) {
// CreatePosRange returns a pair of RGATreeSplitNodePos of the given integer offsets.
func (t *Text) CreatePosRange(from, to int) (*RGATreeSplitNodePos, *RGATreeSplitNodePos, error) {
return t.rgaTreeSplit.createRange(from, to)
}

// CreateBoundaryRange returns a pair of RGATreeSplitNodeBoundary of the given pos.
func (t *Text) CreateBoundaryRange(from, to *RGATreeSplitNodePos, editedAt *time.Ticket) (*RGATreeSplitNodeBoundary, *RGATreeSplitNodeBoundary, error) {
_, fromRight, err := t.rgaTreeSplit.findNodeWithSplit(from, editedAt)
if err != nil {
return nil, nil, err
}
_, toRight, err := t.rgaTreeSplit.findNodeWithSplit(to, editedAt)
if err != nil {
return nil, nil, err
}
return NewRGATreeSplitNodeBoundary(fromRight.ID(), ""), NewRGATreeSplitNodeBoundary(toRight.ID(), ""), nil
}

// Edit edits the given range with the given content and attributes.
func (t *Text) Edit(
from,
Expand All @@ -249,23 +262,37 @@ func (t *Text) Edit(
// Style applies the given attributes of the given range.
func (t *Text) Style(
from,
to *RGATreeSplitNodePos,
to *RGATreeSplitNodeBoundary,
latestCreatedAtMapByActor map[string]*time.Ticket,
attributes map[string]string,
executedAt *time.Ticket,
) (map[string]*time.Ticket, error) {
// 01. Split nodes with from and to
_, toRight, err := t.rgaTreeSplit.findNodeWithSplit(to, executedAt)
if err != nil {
return nil, err
}
_, fromRight, err := t.rgaTreeSplit.findNodeWithSplit(from, executedAt)
if err != nil {
return nil, err
// 01. Split nodes with boundaryRange if it is a remote operation
isRemote := latestCreatedAtMapByActor != nil
if isRemote {
err := t.rgaTreeSplit.splitNodeByBoundary(to)
if err != nil {
return nil, err
}
err = t.rgaTreeSplit.splitNodeByBoundary(from)
if err != nil {
return nil, err
}
}

// 02. style nodes between from and to
nodes := t.rgaTreeSplit.findBetween(fromRight, toRight)
if from.Type() != "" && to.Type() != "" {
// 02-1. Update styleOpsBefore and styleOpsAfter if it is a bold type
// TODO(MoonGyu1): Peritext 2. Update styleOpsBefore/styleOpsAfter of fromRight/toRight nodes

createdAtMapByActor := make(map[string]*time.Ticket)
return createdAtMapByActor, nil
}
// 02-2. Apply the existing logic to style nodes if they are not of a bold type
fromNode := t.rgaTreeSplit.FindNode(from.ID())
toNode := t.rgaTreeSplit.FindNode(to.ID())

nodes := t.rgaTreeSplit.findBetween(fromNode, toNode)
createdAtMapByActor := make(map[string]*time.Ticket)
var toBeStyled []*RGATreeSplitNode[*TextValue]

Expand Down
25 changes: 13 additions & 12 deletions pkg/document/crdt/text_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ func TestText(t *testing.T) {
ctx := helper.TextChangeContext(root)
text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket())

fromPos, toPos, _ := text.CreateRange(0, 0)
fromPos, toPos, _ := text.CreatePosRange(0, 0)
_, _, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
assert.Equal(t, `[{"val":"Hello World"}]`, text.Marshal())

fromPos, toPos, _ = text.CreateRange(6, 11)
fromPos, toPos, _ = text.CreatePosRange(6, 11)
_, _, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
assert.Equal(t, `[{"val":"Hello "},{"val":"Yorkie"}]`, text.Marshal())
Expand Down Expand Up @@ -69,23 +69,24 @@ func TestText(t *testing.T) {
ctx := helper.TextChangeContext(root)
text := crdt.NewText(crdt.NewRGATreeSplit(crdt.InitialTextNode()), ctx.IssueTimeTicket())

fromPos, toPos, _ := text.CreateRange(0, 0)
fromPos, toPos, _ := text.CreatePosRange(0, 0)
_, _, err := text.Edit(fromPos, toPos, nil, "Hello World", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
assert.Equal(t, `[{"val":"Hello World"}]`, text.Marshal())

fromPos, toPos, _ = text.CreateRange(6, 11)
fromPos, toPos, _ = text.CreatePosRange(6, 11)
_, _, err = text.Edit(fromPos, toPos, nil, "Yorkie", nil, ctx.IssueTimeTicket())
assert.NoError(t, err)
assert.Equal(t, `[{"val":"Hello "},{"val":"Yorkie"}]`, text.Marshal())

fromPos, toPos, _ = text.CreateRange(0, 1)
_, err = text.Style(fromPos, toPos, nil, map[string]string{"b": "1"}, ctx.IssueTimeTicket())
assert.NoError(t, err)
assert.Equal(
t,
`[{"attrs":{"b":"1"},"val":"H"},{"val":"ello "},{"val":"Yorkie"}]`,
text.Marshal(),
)
// TODO(MoonGyu1): Remove annotation after implementing addMark operation
// fromPos, toPos, _ = text.CreatePosRange(0, 1)
// _, err = text.Style(fromPos, toPos, nil, map[string]string{"b": "1"}, ctx.IssueTimeTicket())
// assert.NoError(t, err)
// assert.Equal(
// t,
// `[{"attrs":{"b":"1"},"val":"H"},{"val":"ello "},{"val":"Yorkie"}]`,
// text.Marshal(),
// )
})
}
Loading

0 comments on commit f14c165

Please sign in to comment.