diff --git a/src/main/java/me/mrletsplay/shareclientcore/connection/DummyConnection.java b/src/main/java/me/mrletsplay/shareclientcore/connection/DummyConnection.java index 63635cd..2d10b87 100644 --- a/src/main/java/me/mrletsplay/shareclientcore/connection/DummyConnection.java +++ b/src/main/java/me/mrletsplay/shareclientcore/connection/DummyConnection.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.function.Consumer; import me.mrletsplay.shareclientcore.connection.message.Message; +import me.mrletsplay.shareclientcore.debug.DebugValues; public class DummyConnection implements RemoteConnection { @@ -67,4 +68,9 @@ public class DummyConnection implements RemoteConnection { } + @Override + public DebugValues getDebugValues() { + return null; + } + } diff --git a/src/main/java/me/mrletsplay/shareclientcore/connection/RemoteConnection.java b/src/main/java/me/mrletsplay/shareclientcore/connection/RemoteConnection.java index 4290161..e2a7d27 100644 --- a/src/main/java/me/mrletsplay/shareclientcore/connection/RemoteConnection.java +++ b/src/main/java/me/mrletsplay/shareclientcore/connection/RemoteConnection.java @@ -1,12 +1,19 @@ package me.mrletsplay.shareclientcore.connection; import me.mrletsplay.shareclientcore.connection.message.Message; +import me.mrletsplay.shareclientcore.debug.DebugValues; /** * Represents a connection to a remote user or server */ public interface RemoteConnection { + public static final String + DEBUG_MESSAGES_SENT = "messagesSent", + DEBUG_MESSAGES_RECEIVED = "messagesReceived", + DEBUG_CHANGES_SENT = "changesSent", + DEBUG_CHANGES_RECEIVED = "changesReceived"; + public static final int PROTOCOL_VERSION = 1; public void connect(String sessionID) throws ConnectionException; @@ -23,4 +30,6 @@ public interface RemoteConnection { public void setDisconnectListener(DisconnectListener listener); + public DebugValues getDebugValues(); + } diff --git a/src/main/java/me/mrletsplay/shareclientcore/connection/WebSocketConnection.java b/src/main/java/me/mrletsplay/shareclientcore/connection/WebSocketConnection.java index bb7b746..7b93358 100644 --- a/src/main/java/me/mrletsplay/shareclientcore/connection/WebSocketConnection.java +++ b/src/main/java/me/mrletsplay/shareclientcore/connection/WebSocketConnection.java @@ -14,9 +14,11 @@ import org.java_websocket.client.WebSocketClient; import org.java_websocket.framing.CloseFrame; import org.java_websocket.handshake.ServerHandshake; +import me.mrletsplay.shareclientcore.connection.message.ChangeMessage; import me.mrletsplay.shareclientcore.connection.message.ClientHelloMessage; import me.mrletsplay.shareclientcore.connection.message.Message; import me.mrletsplay.shareclientcore.connection.message.ServerHelloMessage; +import me.mrletsplay.shareclientcore.debug.DebugValues; public class WebSocketConnection implements RemoteConnection { @@ -29,11 +31,18 @@ public class WebSocketConnection implements RemoteConnection { private Object wait = new Object(); private boolean helloReceived; private ConnectionException connectException; + private DebugValues debugValues; public WebSocketConnection(URI uri, String username, Map httpHeaders) { this.client = new WSClient(uri, httpHeaders); this.username = username; this.listeners = new HashSet<>(); + this.debugValues = new DebugValues( + DEBUG_MESSAGES_SENT, + DEBUG_MESSAGES_RECEIVED, + DEBUG_CHANGES_SENT, + DEBUG_CHANGES_RECEIVED + ); } public WebSocketConnection(URI uri, String username) { @@ -68,6 +77,9 @@ public class WebSocketConnection implements RemoteConnection { @Override public void send(Message message) throws ConnectionException { + debugValues.increment(DEBUG_MESSAGES_SENT); + if(message instanceof ChangeMessage) debugValues.increment(DEBUG_CHANGES_SENT); + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); DataOutputStream dOut = new DataOutputStream(bOut); @@ -96,6 +108,11 @@ public class WebSocketConnection implements RemoteConnection { this.disconnectListener = listener; } + @Override + public DebugValues getDebugValues() { + return debugValues; + } + private class WSClient extends WebSocketClient { public WSClient(URI serverUri) { @@ -126,6 +143,9 @@ public class WebSocketConnection implements RemoteConnection { return; } + debugValues.increment(DEBUG_MESSAGES_RECEIVED); + if(m instanceof ChangeMessage) debugValues.increment(DEBUG_CHANGES_RECEIVED); + if(m instanceof ServerHelloMessage hello) { helloReceived = true; siteID = hello.siteID(); diff --git a/src/main/java/me/mrletsplay/shareclientcore/connection/message/FullSyncMessage.java b/src/main/java/me/mrletsplay/shareclientcore/connection/message/FullSyncMessage.java index 6b6e07d..dc43ce8 100644 --- a/src/main/java/me/mrletsplay/shareclientcore/connection/message/FullSyncMessage.java +++ b/src/main/java/me/mrletsplay/shareclientcore/connection/message/FullSyncMessage.java @@ -35,7 +35,6 @@ public record FullSyncMessage(int siteID, String documentPath, List conten List content = new ArrayList<>(contentSize); for(int i = 0; i < contentSize; i++) { content.add(Char.deserialize(in)); - System.out.println(content.get(content.size() - 1)); } return new FullSyncMessage(siteID, documentPath, content); diff --git a/src/main/java/me/mrletsplay/shareclientcore/debug/DebugValues.java b/src/main/java/me/mrletsplay/shareclientcore/debug/DebugValues.java new file mode 100644 index 0000000..dc422dc --- /dev/null +++ b/src/main/java/me/mrletsplay/shareclientcore/debug/DebugValues.java @@ -0,0 +1,66 @@ +package me.mrletsplay.shareclientcore.debug; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class DebugValues { + + private Set keys; + private Map values; + + public DebugValues(Collection keys) { + this.keys = Collections.unmodifiableSet(new LinkedHashSet<>(keys)); + this.values = new HashMap<>(this.keys.size()); + } + + public DebugValues(String... keys) { + this(Arrays.asList(keys)); + } + + public Set getKeys() { + return keys; + } + + private void checkKey(String key) throws IllegalArgumentException { + if(!keys.contains(key)) throw new IllegalArgumentException("Not a valid key"); + } + + public void set(String key, int value) throws IllegalArgumentException { + checkKey(key); + values.put(key, value); + } + + public int get(String key) throws IllegalArgumentException { + checkKey(key); + return values.getOrDefault(key, 0); + } + + public void reset(String key) throws IllegalArgumentException { + checkKey(key); + values.remove(key); + } + + public void increment(String key) throws IllegalArgumentException { + checkKey(key); + values.put(key, values.getOrDefault(key, 0) + 1); + } + + public void decrement(String key) throws IllegalArgumentException { + checkKey(key); + values.put(key, values.getOrDefault(key, 0) + 1); + } + + @Override + public String toString() { + return keys.stream() + .map(key -> key + ": " + get(key)) + .collect(Collectors.joining("\n")); + } + +} diff --git a/src/main/java/me/mrletsplay/shareclientcore/document/ArrayCharBag.java b/src/main/java/me/mrletsplay/shareclientcore/document/ArrayCharBag.java index cbfc2b9..fb0e162 100644 --- a/src/main/java/me/mrletsplay/shareclientcore/document/ArrayCharBag.java +++ b/src/main/java/me/mrletsplay/shareclientcore/document/ArrayCharBag.java @@ -2,27 +2,42 @@ package me.mrletsplay.shareclientcore.document; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import me.mrletsplay.shareclientcore.debug.DebugValues; + public class ArrayCharBag implements CharBag { private List chars; - public ArrayCharBag() { - this.chars = new ArrayList<>(); - } + private DebugValues debugValues; public ArrayCharBag(List chars) { this.chars = new ArrayList<>(chars); + this.debugValues = new DebugValues( + DEBUG_INSERTIONS, + DEBUG_INSERTIONS_DROPPED, + DEBUG_DELETIONS, + DEBUG_DELETIONS_DROPPED + ); + } + + public ArrayCharBag() { + this(Collections.emptyList()); } @Override public int add(Char character) { int i = 0; // TODO: use binary search - while(i < chars.size() && Util.comparePositions(chars.get(i).position(), character.position()) < 0) i++; - if(i < chars.size() && Util.comparePositions(chars.get(i).position(), character.position()) == 0) return -1; + while(i < chars.size() && Util.compareChars(chars.get(i), character) < 0) i++; + if(i < chars.size() && Util.compareChars(chars.get(i), character) == 0) { + debugValues.increment(DEBUG_INSERTIONS_DROPPED); + return -1; + } chars.add(i, character); + debugValues.increment(DEBUG_INSERTIONS); return i; } @@ -30,9 +45,13 @@ public class ArrayCharBag implements CharBag { public int remove(Char character) { int i = 0; // TODO: use binary search - while(i < chars.size() && Util.comparePositions(chars.get(i).position(), character.position()) < 0) i++; - if(i == chars.size() || Util.comparePositions(chars.get(i).position(), character.position()) != 0) return -1; + while(i < chars.size() && Util.compareChars(chars.get(i), character) < 0) i++; + if(i == chars.size() || Util.compareChars(chars.get(i), character) != 0) { + debugValues.increment(DEBUG_DELETIONS_DROPPED); + return -1; + } chars.remove(i); + debugValues.increment(DEBUG_DELETIONS); return i; } @@ -70,4 +89,9 @@ public class ArrayCharBag implements CharBag { return new String(getContents(), StandardCharsets.UTF_8); } + @Override + public DebugValues getDebugValues() { + return debugValues; + } + } diff --git a/src/main/java/me/mrletsplay/shareclientcore/document/CharBag.java b/src/main/java/me/mrletsplay/shareclientcore/document/CharBag.java index 99dfbbd..57b3d87 100644 --- a/src/main/java/me/mrletsplay/shareclientcore/document/CharBag.java +++ b/src/main/java/me/mrletsplay/shareclientcore/document/CharBag.java @@ -3,8 +3,20 @@ package me.mrletsplay.shareclientcore.document; import java.nio.charset.StandardCharsets; import java.util.List; +import me.mrletsplay.shareclientcore.debug.DebugValues; + public interface CharBag { + /** + * Debug keys + * @see #getDebugValues() + */ + public static final String + DEBUG_INSERTIONS = "insertions", + DEBUG_INSERTIONS_DROPPED = "insertionsDropped", + DEBUG_DELETIONS = "deletions", + DEBUG_DELETIONS_DROPPED = "deletionsDropped"; + /** * Adds a character to the bag and returns the index it was inserted at, or -1 if it was not inserted because it already exists * @param character The character to add @@ -39,7 +51,7 @@ public interface CharBag { /** * Collects the chars in this bag ordered by their position into a list - * @return + * @return The chars in this bag */ public List toList(); @@ -56,4 +68,13 @@ public interface CharBag { */ public String getContentsAsString(); + /** + * @return Debug information about this bag + * @see #DEBUG_INSERTIONS + * @see #DEBUG_INSERTIONS_DROPPED + * @see #DEBUG_DELETIONS + * @see #DEBUG_DELETIONS_DROPPED + */ + public DebugValues getDebugValues(); + } diff --git a/src/main/java/me/mrletsplay/shareclientcore/document/SharedDocument.java b/src/main/java/me/mrletsplay/shareclientcore/document/SharedDocument.java index 8aa6b33..f059630 100644 --- a/src/main/java/me/mrletsplay/shareclientcore/document/SharedDocument.java +++ b/src/main/java/me/mrletsplay/shareclientcore/document/SharedDocument.java @@ -49,7 +49,7 @@ public class SharedDocument implements MessageListener { if(index < 0 || index >= charBag.size() - 1) throw new IllegalArgumentException("Index out of bounds"); Char charBefore = charBag.get(index); - Char charAfter = charBag.get(index +1); + Char charAfter = charBag.get(index + 1); Change[] changes = new Change[bytes.length]; for(int i = 0; i < bytes.length; i++) { @@ -113,7 +113,6 @@ public class SharedDocument implements MessageListener { if(charBag.remove(toRemove) == -1) throw new IllegalStateException("Couldn't remove existing char"); } - for(Change c : changes) { try { connection.send(new ChangeMessage(c)); diff --git a/src/main/java/me/mrletsplay/shareclientcore/document/Util.java b/src/main/java/me/mrletsplay/shareclientcore/document/Util.java index 3257f31..bb05b7a 100644 --- a/src/main/java/me/mrletsplay/shareclientcore/document/Util.java +++ b/src/main/java/me/mrletsplay/shareclientcore/document/Util.java @@ -19,6 +19,12 @@ public class Util { return Integer.compare(a.length, b.length); } + public static int compareChars(Char a, Char b) { + int pos = comparePositions(a.position(), b.position()); + if(pos != 0) return pos; + return Integer.compare(a.lamport(), b.lamport()); + } + public static Identifier[] generatePositionBetween(Identifier[] before, Identifier[] after, int site) { if(comparePositions(before, after) != -1) throw new IllegalArgumentException("before must be strictly less than after"); @@ -28,7 +34,8 @@ public class Util { Identifier c1 = i >= before.length ? new Identifier(0, site) : before[i]; Identifier c2 = i >= after.length ? new Identifier(BASE - 1, site) : after[i]; - if(c1.digit() != c2.digit()) { + // Modification to the original source because otherwise it can lead to cases where positions can't be generated and this seems to work + if(i >= before.length || c1.digit() != c2.digit()) { int[] incremented = getIncremented(before, after, i); return constructPosition(incremented, before, after, site); } diff --git a/src/test/java/me/mrletsplay/shareclientcore/SharingTest.java b/src/test/java/me/mrletsplay/shareclientcore/SharingTest.java index e5cb6ee..40eeba2 100644 --- a/src/test/java/me/mrletsplay/shareclientcore/SharingTest.java +++ b/src/test/java/me/mrletsplay/shareclientcore/SharingTest.java @@ -63,40 +63,49 @@ public class SharingTest { String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890"; Random r = new Random(0); - for(int i = 0; i < 1000; i++) { + for(int i = 0; i < 10_000; i++) { switch(r.nextInt(sharedA.getContents().length == 0 ? 2 : 4)) { case 0: { // Insert A char[] insert = new char[r.nextInt(16)]; for(int j = 0; j < insert.length; j++) insert[j] = chars.charAt(r.nextInt(chars.length())); sharedA.localInsert(r.nextInt(sharedA.getContents().length + 1), String.valueOf(insert)); + break; } case 1: { // Insert B char[] insert = new char[r.nextInt(16)]; for(int j = 0; j < insert.length; j++) insert[j] = chars.charAt(r.nextInt(chars.length())); sharedB.localInsert(r.nextInt(sharedB.getContents().length + 1), String.valueOf(insert)); + break; } case 2: { // Delete A int len = sharedA.getContents().length; int idx = r.nextInt(len); - int n = r.nextInt(len - idx); + int n = r.nextInt(Math.min(16, len - idx)) + 1; sharedA.localDelete(idx, n); + break; } case 3: { // Delete B int len = sharedB.getContents().length; int idx = r.nextInt(len); - int n = r.nextInt(len - idx + 1); + int n = r.nextInt(Math.min(16, len - idx)) + 1; sharedB.localDelete(idx, n); + break; } } - System.out.println("A: " + sharedA.getContentsAsString()); - System.out.println("B: " + sharedB.getContentsAsString()); +// System.out.println("A: " + sharedA.getContentsAsString()); +// System.out.println("B: " + sharedB.getContentsAsString()); assertEquals(sharedA.getContentsAsString(), sharedB.getContentsAsString()); } } + @Test + public void testSharedDocumentInterleaving() { + // TODO: test random interleaving of messages + } + }