diff --git a/centrifuge/api.txt b/centrifuge/api.txt index bf8adaf..e4c17e6 100644 --- a/centrifuge/api.txt +++ b/centrifuge/api.txt @@ -9,6 +9,7 @@ package io.github.centrifugal.centrifuge { method public io.github.centrifugal.centrifuge.ClientState! getState(); method public io.github.centrifugal.centrifuge.Subscription! getSubscription(String); method public void history(String, io.github.centrifugal.centrifuge.HistoryOptions!, io.github.centrifugal.centrifuge.ResultCallback!); + method public io.github.centrifugal.centrifuge.Subscription! newSubscription(String, io.github.centrifugal.centrifuge.SubscriptionOptions!, io.github.centrifugal.centrifuge.SubscriptionEventListener!) throws io.github.centrifugal.centrifuge.DuplicateSubscriptionException; method public io.github.centrifugal.centrifuge.Subscription! newSubscription(String, io.github.centrifugal.centrifuge.SubscriptionEventListener!) throws io.github.centrifugal.centrifuge.DuplicateSubscriptionException; method public void presence(String, io.github.centrifugal.centrifuge.ResultCallback!); method public void presenceStats(String, io.github.centrifugal.centrifuge.ResultCallback!); @@ -16,6 +17,7 @@ package io.github.centrifugal.centrifuge { method public void removeSubscription(io.github.centrifugal.centrifuge.Subscription!); method public void rpc(String, byte[]!, io.github.centrifugal.centrifuge.ResultCallback!); method public void send(byte[]!, io.github.centrifugal.centrifuge.CompletionCallback!); + method public void setToken(String); } public class ClientInfo { @@ -46,6 +48,10 @@ package io.github.centrifugal.centrifuge { method public void onDone(Throwable); } + public class ConfigurationError { + method public Throwable getError(); + } + public class ConnectedEvent { ctor public ConnectedEvent(); method public String getClient(); @@ -82,6 +88,7 @@ package io.github.centrifugal.centrifuge { public class ErrorEvent { method public Throwable getError(); + method public Integer getHttpResponseCode(); } public abstract class EventListener { @@ -99,6 +106,17 @@ package io.github.centrifugal.centrifuge { method public void onUnsubscribed(io.github.centrifugal.centrifuge.Client!, io.github.centrifugal.centrifuge.ServerUnsubscribedEvent!); } + public class Fossil { + ctor public Fossil(); + method public static byte[]! applyDelta(byte[]!, byte[]!); + method public static long checksum(byte[]!); + } + + public class FossilTest { + ctor public FossilTest(); + method public void testApplyDelta(); + } + public class HistoryOptions { method public int getLimit(); method public boolean getReverse(); @@ -182,6 +200,7 @@ package io.github.centrifugal.centrifuge { public class Publication { ctor public Publication(); method public byte[]! getData(); + method public io.github.centrifugal.centrifuge.ClientInfo! getInfo(); method public long getOffset(); } @@ -284,11 +303,13 @@ package io.github.centrifugal.centrifuge { public class Subscription { method public String getChannel(); + method public byte[]! getPrevData(); method public io.github.centrifugal.centrifuge.SubscriptionState! getState(); method public void history(io.github.centrifugal.centrifuge.HistoryOptions!, io.github.centrifugal.centrifuge.ResultCallback!); method public void presence(io.github.centrifugal.centrifuge.ResultCallback!); method public void presenceStats(io.github.centrifugal.centrifuge.ResultCallback!); method public void publish(byte[]!, io.github.centrifugal.centrifuge.ResultCallback!); + method public void setPrevData(byte[]!); method public void subscribe(); method public void unsubscribe(); } @@ -311,6 +332,7 @@ package io.github.centrifugal.centrifuge { public class SubscriptionOptions { ctor public SubscriptionOptions(); method public byte[]! getData(); + method public String getDelta(); method public int getMaxResubscribeDelay(); method public int getMinResubscribeDelay(); method public String getToken(); @@ -319,6 +341,7 @@ package io.github.centrifugal.centrifuge { method public boolean isPositioned(); method public boolean isRecoverable(); method public void setData(byte[]!); + method public void setDelta(String); method public void setJoinLeave(boolean); method public void setMaxResubscribeDelay(int); method public void setMinResubscribeDelay(int); @@ -369,6 +392,14 @@ package io.github.centrifugal.centrifuge { method public Throwable getError(); } + public class UnauthorizedException { + ctor public UnauthorizedException(); + } + + public class UnclassifiedError { + method public Throwable getError(); + } + public class UnsubscribedEvent { ctor public UnsubscribedEvent(int, String); method public int getCode(); diff --git a/centrifuge/src/main/java/io/github/centrifugal/centrifuge/Client.java b/centrifuge/src/main/java/io/github/centrifugal/centrifuge/Client.java index fc9fc12..285b5dc 100644 --- a/centrifuge/src/main/java/io/github/centrifugal/centrifuge/Client.java +++ b/centrifuge/src/main/java/io/github/centrifugal/centrifuge/Client.java @@ -855,7 +855,7 @@ private void failUnauthorized() { this.processDisconnect(DISCONNECTED_UNAUTHORIZED, "unauthorized", false); } - private void processReply(Protocol.Reply reply) { + private void processReply(Protocol.Reply reply) throws Exception { if (reply.getId() > 0) { CompletableFuture cf = this.futures.get(reply.getId()); if (cf != null) cf.complete(reply); @@ -868,12 +868,18 @@ private void processReply(Protocol.Reply reply) { } } - private void handlePub(String channel, Protocol.Publication pub) { + private void handlePub(String channel, Protocol.Publication pub) throws Exception { ClientInfo info = ClientInfo.fromProtocolClientInfo(pub.getInfo()); Subscription sub = this.getSub(channel); if (sub != null) { PublicationEvent event = new PublicationEvent(); - event.setData(pub.getData().toByteArray()); + byte[] pubData = pub.getData().toByteArray(); + byte[] prevData = sub.getPrevData(); + if (prevData != null && pub.getDelta()) { + pubData = Fossil.applyDelta(prevData, pubData); + } + sub.setPrevData(pubData); + event.setData(pubData); event.setInfo(info); event.setOffset(pub.getOffset()); event.setTags(pub.getTagsMap()); @@ -974,7 +980,7 @@ private void handleDisconnect(Protocol.Disconnect disconnect) { } } - private void handlePush(Protocol.Push push) { + private void handlePush(Protocol.Push push) throws Exception { String channel = push.getChannel(); if (push.hasPub()) { this.handlePub(channel, push.getPub()); diff --git a/centrifuge/src/main/java/io/github/centrifugal/centrifuge/Fossil.java b/centrifuge/src/main/java/io/github/centrifugal/centrifuge/Fossil.java new file mode 100644 index 0000000..946a23d --- /dev/null +++ b/centrifuge/src/main/java/io/github/centrifugal/centrifuge/Fossil.java @@ -0,0 +1,191 @@ +package io.github.centrifugal.centrifuge; + +import java.io.ByteArrayOutputStream; + + +public class Fossil { + + private static final int[] zValue = new int[] { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, + -1, 36, -1, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1, + }; + + // Reader class + static class Reader { + private byte[] a; + private int pos; + + public Reader(byte[] array) { + this.a = array; + this.pos = 0; + } + + public boolean haveBytes() { + return this.pos < this.a.length; + } + + public int getByte() { + if (this.pos >= this.a.length) { + throw new IndexOutOfBoundsException("out of bounds"); + } + int b = this.a[this.pos++] & 0xFF; + return b; + } + + public char getChar() { + return (char) getByte(); + } + + public int getInt() { + int v = 0; + int c; + while (haveBytes() && (c = zValue[getByte() & 0x7F]) >= 0) { + v = (v << 6) + c; + } + this.pos--; + return v; + } + } + + // Writer class + static class Writer { + private ByteArrayOutputStream a = new ByteArrayOutputStream(); + + public byte[] toByteArray() { + return a.toByteArray(); + } + + // Copy from array 'arr' from 'start' to 'end' (exclusive) + public void putArray(byte[] arr, int start, int end) { + if (start < 0 || end > arr.length || start > end) { + throw new IndexOutOfBoundsException("Invalid start or end index"); + } + a.write(arr, start, end - start); + } + } + + // Checksum function + public static long checksum(byte[] arr) { + int sum0 = 0, sum1 = 0, sum2 = 0, sum3 = 0; + int z = 0; + int N = arr.length; + while (N >= 16) { + sum0 += (arr[z + 0] & 0xFF); + sum1 += (arr[z + 1] & 0xFF); + sum2 += (arr[z + 2] & 0xFF); + sum3 += (arr[z + 3] & 0xFF); + + sum0 += (arr[z + 4] & 0xFF); + sum1 += (arr[z + 5] & 0xFF); + sum2 += (arr[z + 6] & 0xFF); + sum3 += (arr[z + 7] & 0xFF); + + sum0 += (arr[z + 8] & 0xFF); + sum1 += (arr[z + 9] & 0xFF); + sum2 += (arr[z + 10] & 0xFF); + sum3 += (arr[z + 11] & 0xFF); + + sum0 += (arr[z + 12] & 0xFF); + sum1 += (arr[z + 13] & 0xFF); + sum2 += (arr[z + 14] & 0xFF); + sum3 += (arr[z + 15] & 0xFF); + + z += 16; + N -= 16; + } + while (N >= 4) { + sum0 += (arr[z + 0] & 0xFF); + sum1 += (arr[z + 1] & 0xFF); + sum2 += (arr[z + 2] & 0xFF); + sum3 += (arr[z + 3] & 0xFF); + z += 4; + N -= 4; + } + sum3 += (sum2 << 8) + (sum1 << 16) + (sum0 << 24); + switch (N) { + case 3: + sum3 += (arr[z + 2] & 0xFF) << 8; + case 2: + sum3 += (arr[z + 1] & 0xFF) << 16; + case 1: + sum3 += (arr[z + 0] & 0xFF) << 24; + break; + default: + break; + } + return sum3 & 0xFFFFFFFFL; + } + + /** + * Apply a delta byte array to a source byte array, returning the target byte array. + */ + public static byte[] applyDelta(byte[] source, byte[] delta) throws Exception { + int total = 0; + Reader zDelta = new Reader(delta); + int lenSrc = source.length; + int lenDelta = delta.length; + + int limit = zDelta.getInt(); + char c = zDelta.getChar(); + if (c != '\n') { + throw new Exception("size integer not terminated by '\\n'"); + } + Writer zOut = new Writer(); + while (zDelta.haveBytes()) { + int cnt = zDelta.getInt(); + int ofst; + + c = zDelta.getChar(); + switch (c) { + case '@': + ofst = zDelta.getInt(); + if (zDelta.haveBytes() && zDelta.getChar() != ',') { + throw new Exception("copy command not terminated by ','"); + } + total += cnt; + if (total > limit) { + throw new Exception("copy exceeds output file size"); + } + if (ofst + cnt > lenSrc) { + throw new Exception("copy extends past end of input"); + } + zOut.putArray(source, ofst, ofst + cnt); + break; + + case ':': + total += cnt; + if (total > limit) { + throw new Exception("insert command gives an output larger than predicted"); + } + if (cnt > lenDelta - zDelta.pos) { + throw new Exception("insert count exceeds size of delta"); + } + zOut.putArray(zDelta.a, zDelta.pos, zDelta.pos + cnt); + zDelta.pos += cnt; + break; + + case ';': + byte[] out = zOut.toByteArray(); + long checksumValue = checksum(out); + if (cnt != (int) checksumValue) { + throw new Exception("bad checksum"); + } + if (total != limit) { + throw new Exception("generated size does not match predicted size"); + } + return out; + + default: + System.out.println(c); + throw new Exception("unknown delta operator"); + } + } + throw new Exception("unterminated delta"); + } + +} diff --git a/centrifuge/src/main/java/io/github/centrifugal/centrifuge/PublicationEvent.java b/centrifuge/src/main/java/io/github/centrifugal/centrifuge/PublicationEvent.java index cf4acc7..f5e3b8e 100644 --- a/centrifuge/src/main/java/io/github/centrifugal/centrifuge/PublicationEvent.java +++ b/centrifuge/src/main/java/io/github/centrifugal/centrifuge/PublicationEvent.java @@ -23,7 +23,6 @@ void setInfo(ClientInfo info) { private ClientInfo info; - public long getOffset() { return offset; } diff --git a/centrifuge/src/main/java/io/github/centrifugal/centrifuge/Subscription.java b/centrifuge/src/main/java/io/github/centrifugal/centrifuge/Subscription.java index 001f9ac..bddec26 100644 --- a/centrifuge/src/main/java/io/github/centrifugal/centrifuge/Subscription.java +++ b/centrifuge/src/main/java/io/github/centrifugal/centrifuge/Subscription.java @@ -27,6 +27,9 @@ public class Subscription { private int resubscribeAttempts = 0; private String token; private com.google.protobuf.ByteString data; + private String delta; + private boolean deltaNegotiated; + private byte[] prevData; Subscription(final Client client, final String channel, final SubscriptionEventListener listener, final SubscriptionOptions options) { this.client = client; @@ -38,6 +41,9 @@ public class Subscription { if (opts.getData() != null) { this.data = com.google.protobuf.ByteString.copyFrom(opts.getData()); } + this.prevData = null; + this.delta = ""; + this.deltaNegotiated = false; } Subscription(final Client client, final String channel, final SubscriptionEventListener listener) { @@ -172,6 +178,7 @@ void moveToSubscribed(Protocol.SubscribeResult result) { this.recover = true; } this.setEpoch(result.getEpoch()); + this.deltaNegotiated = result.getDelta(); byte[] data = null; if (result.getData() != null) { @@ -255,6 +262,7 @@ Protocol.SubscribeRequest createSubscribeRequest() { builder.setPositioned(this.opts.isPositioned()); builder.setRecoverable(this.opts.isRecoverable()); builder.setJoinLeave(this.opts.isJoinLeave()); + builder.setDelta(this.opts.getDelta()); return builder.build(); } @@ -459,4 +467,12 @@ private void presenceStatsSynchronized(ResultCallback cb) { f.complete(null); } } + + public byte[] getPrevData() { + return prevData; + } + + public void setPrevData(byte[] prevData) { + this.prevData = prevData; + } } diff --git a/centrifuge/src/main/java/io/github/centrifugal/centrifuge/SubscriptionOptions.java b/centrifuge/src/main/java/io/github/centrifugal/centrifuge/SubscriptionOptions.java index 2e5d2b2..6aaa541 100644 --- a/centrifuge/src/main/java/io/github/centrifugal/centrifuge/SubscriptionOptions.java +++ b/centrifuge/src/main/java/io/github/centrifugal/centrifuge/SubscriptionOptions.java @@ -85,4 +85,14 @@ public void setJoinLeave(boolean joinLeave) { } private boolean joinLeave = false; + + public String getDelta() { + return delta; + } + + public void setDelta(String delta) { + this.delta = delta; + } + + private String delta = ""; } diff --git a/centrifuge/src/main/proto/client.proto b/centrifuge/src/main/proto/client.proto index faaece7..5fc8ff3 100644 --- a/centrifuge/src/main/proto/client.proto +++ b/centrifuge/src/main/proto/client.proto @@ -103,6 +103,7 @@ message Publication { ClientInfo info = 5; uint64 offset = 6; map tags = 7; + bool delta = 8; } message Join { @@ -199,6 +200,7 @@ message SubscribeRequest { bool positioned = 9; bool recoverable = 10; bool join_leave = 11; + string delta = 12; } message SubscribeResult { @@ -213,6 +215,7 @@ message SubscribeResult { bool positioned = 10; bytes data = 11; bool was_recovering = 12; + bool delta = 13; } message SubRefreshRequest { diff --git a/centrifuge/src/test/java/io/github/centrifugal/centrifuge/FossilTest.java b/centrifuge/src/test/java/io/github/centrifugal/centrifuge/FossilTest.java new file mode 100644 index 0000000..b94b717 --- /dev/null +++ b/centrifuge/src/test/java/io/github/centrifugal/centrifuge/FossilTest.java @@ -0,0 +1,284 @@ +package io.github.centrifugal.centrifuge; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +import static org.junit.Assert.*; + + +public class FossilTest { + + @Test + public void testApplyDelta() throws Exception { + // Test 1 + String source1_str = "{\"asks\":[[\"609590\",\"3792.6\"],[\"609600\",\"11507.8\"],[\"609640\",\"663.11\"]," + + "[\"609690\",\"302.71\"],[\"609700\",\"744.52\"],[\"609730\",\"209.94\"]," + + "[\"609750\",\"18.59\"],[\"609790\",\"156\"],[\"609800\",\"859.03\"],[\"609830\"," + + "\"216.98\"],[\"609860\",\"217.42\"],[\"609870\",\"60\"],[\"609880\",\"384.06\"]," + + "[\"609890\",\"4615.87\"],[\"609900\",\"25.98\"],[\"609940\",\"63.95\"],[\"609950\"," + + "\"242.6\"],[\"609960\",\"2000\"],[\"609970\",\"1573\"],[\"609980\",\"47.56\"]," + + "[\"609990\",\"582.26\"],[\"610000\",\"42899.13\"],[\"610020\",\"24.46\"]," + + "[\"610110\",\"150\"]]," + + "\"bids\":[[\"609520\",\"2010.12\"],[\"609510\",\"5080.7\"],[\"609500\",\"297.5\"]," + + "[\"609490\",\"1238.52\"],[\"609480\",\"896.37\"],[\"609470\",\"1234.91\"]," + + "[\"609460\",\"451.36\"],[\"609250\",\"58.45\"],[\"609220\",\"786.48\"]," + + "[\"609200\",\"101.64\"],[\"609190\",\"41.03\"],[\"609160\",\"650.49\"]," + + "[\"609100\",\"6932.07\"],[\"609070\",\"16.59\"],[\"609050\",\"149.22\"]," + + "[\"609040\",\"52.53\"],[\"609030\",\"11.52\"],[\"609020\",\"1038.35\"]," + + "[\"609010\",\"334.83\"],[\"609000\",\"3453.95\"],[\"608900\",\"850.81\"]," + + "[\"608880\",\"57\"],[\"608850\",\"5.47\"],[\"608840\",\"41.23\"]]," + + "\"lastTradePrice\":\"609520\",\"lastUpdate\":1727632299611}"; + + String delta1_str = "Fm\n7i@0,6:1852.77s@7o,7:303488}3~2wv0;"; + + String out1 = "{\"asks\":[[\"609590\",\"3792.6\"],[\"609600\",\"11507.8\"],[\"609640\",\"663.11\"]," + + "[\"609690\",\"302.71\"],[\"609700\",\"744.52\"],[\"609730\",\"209.94\"]," + + "[\"609750\",\"18.59\"],[\"609790\",\"156\"],[\"609800\",\"859.03\"]," + + "[\"609830\",\"216.98\"],[\"609860\",\"217.42\"],[\"609870\",\"60\"]," + + "[\"609880\",\"384.06\"],[\"609890\",\"4615.87\"],[\"609900\",\"25.98\"]," + + "[\"609940\",\"63.95\"],[\"609950\",\"242.6\"],[\"609960\",\"2000\"]," + + "[\"609970\",\"1573\"],[\"609980\",\"47.56\"],[\"609990\",\"582.26\"]," + + "[\"610000\",\"42899.13\"],[\"610020\",\"24.46\"],[\"610110\",\"150\"]]," + + "\"bids\":[[\"609520\",\"1852.72\"],[\"609510\",\"5080.7\"],[\"609500\",\"297.5\"]," + + "[\"609490\",\"1238.52\"],[\"609480\",\"896.37\"],[\"609470\",\"1234.91\"]," + + "[\"609460\",\"451.36\"],[\"609250\",\"58.45\"],[\"609220\",\"786.48\"]," + + "[\"609200\",\"101.64\"],[\"609190\",\"41.03\"],[\"609160\",\"650.49\"]," + + "[\"609100\",\"6932.07\"],[\"609070\",\"16.59\"],[\"609050\",\"149.22\"]," + + "[\"609040\",\"52.53\"],[\"609030\",\"11.52\"],[\"609020\",\"1038.35\"]," + + "[\"609010\",\"334.83\"],[\"609000\",\"3453.95\"],[\"608900\",\"850.81\"]," + + "[\"608880\",\"57\"],[\"608850\",\"5.47\"],[\"608840\",\"41.23\"]]," + + "\"lastTradePrice\":\"609520\",\"lastUpdate\":1727632303488}"; + + byte[] result_bytes = Fossil.applyDelta( + source1_str.getBytes("UTF-8"), + delta1_str.getBytes("UTF-8") + ); + assertEquals(out1, new String(result_bytes, "UTF-8")); + + // Test 2 + String source2_str = "{\"asks\":[[\"610480\",\"26.96\"],[\"610490\",\"32.76\"],[\"610500\",\"622.44\"]," + + "[\"610530\",\"238.7\"],[\"610540\",\"990.9\"],[\"610830\",\"33\"]," + + "[\"610840\",\"159.9\"],[\"610880\",\"100\"],[\"610890\",\"33\"]," + + "[\"610900\",\"913.87\"],[\"610920\",\"30.52\"],[\"610970\",\"480.74\"]," + + "[\"610980\",\"1500\"],[\"610990\",\"266.61\"],[\"611000\",\"9672.99\"]," + + "[\"611100\",\"404.63\"],[\"611150\",\"56.25\"],[\"611170\",\"2011.71\"]," + + "[\"611200\",\"25.2\"],[\"611210\",\"10.17\"],[\"611240\",\"150\"]," + + "[\"611320\",\"8.27\"],[\"611350\",\"76.6\"],[\"611400\",\"777.8\"]]," + + "\"bids\":[[\"610360\",\"115.64\"],[\"610350\",\"525.38\"],[\"610340\",\"575.77\"]," + + "[\"610330\",\"421.83\"],[\"610320\",\"1943.17\"],[\"610310\",\"241.36\"]," + + "[\"610300\",\"3186.21\"],[\"610080\",\"418.23\"],[\"610050\",\"167.12\"]," + + "[\"610030\",\"30\"],[\"610010\",\"31.14\"],[\"610000\",\"2989.86\"]," + + "[\"609920\",\"85.04\"],[\"609910\",\"58.72\"],[\"609900\",\"2.05\"]," + + "[\"609730\",\"50\"],[\"609700\",\"729.81\"],[\"609690\",\"3608.06\"]," + + "[\"609590\",\"3.48\"],[\"609580\",\"17.48\"],[\"609520\",\"163.92\"]," + + "[\"609510\",\"500\"],[\"609500\",\"3802.96\"],[\"609460\",\"86.27\"]]," + + "\"lastTradePrice\":\"610490\",\"lastUpdate\":1727679775574}"; + + String delta2_str = "FS\nEx@0,2:36P@Ez,5:6413}icT15;"; + + String out2 = "{\"asks\":[[\"610480\",\"26.96\"],[\"610490\",\"32.76\"],[\"610500\",\"622.44\"]," + + "[\"610530\",\"238.7\"],[\"610540\",\"990.9\"],[\"610830\",\"33\"]," + + "[\"610840\",\"159.9\"],[\"610880\",\"100\"],[\"610890\",\"33\"]," + + "[\"610900\",\"913.87\"],[\"610920\",\"30.52\"],[\"610970\",\"480.74\"]," + + "[\"610980\",\"1500\"],[\"610990\",\"266.61\"],[\"611000\",\"9672.99\"]," + + "[\"611100\",\"404.63\"],[\"611150\",\"56.25\"],[\"611170\",\"2011.71\"]," + + "[\"611200\",\"25.2\"],[\"611210\",\"10.17\"],[\"611240\",\"150\"]," + + "[\"611320\",\"8.27\"],[\"611350\",\"76.6\"],[\"611400\",\"777.8\"]]," + + "\"bids\":[[\"610360\",\"115.64\"],[\"610350\",\"525.38\"],[\"610340\",\"575.77\"]," + + "[\"610330\",\"421.83\"],[\"610320\",\"1943.17\"],[\"610310\",\"241.36\"]," + + "[\"610300\",\"3186.21\"],[\"610080\",\"418.23\"],[\"610050\",\"167.12\"]," + + "[\"610030\",\"30\"],[\"610010\",\"31.14\"],[\"610000\",\"2989.86\"]," + + "[\"609920\",\"85.04\"],[\"609910\",\"58.72\"],[\"609900\",\"2.05\"]," + + "[\"609730\",\"50\"],[\"609700\",\"729.81\"],[\"609690\",\"3608.06\"]," + + "[\"609590\",\"3.48\"],[\"609580\",\"17.48\"],[\"609520\",\"163.92\"]," + + "[\"609510\",\"500\"],[\"609500\",\"3802.96\"],[\"609460\",\"86.27\"]]," + + "\"lastTradePrice\":\"610360\",\"lastUpdate\":1727679776413}"; + + result_bytes = Fossil.applyDelta( + source2_str.getBytes("UTF-8"), + delta2_str.getBytes("UTF-8") + ); + assertEquals(out2, new String(result_bytes, "UTF-8")); + + // Test 3 + String source3_str = "{\"asks\":[[\"610350\",\"3422.1\"],[\"610380\",\"1743.7\"],[\"610400\",\"5133.73\"]," + + "[\"610410\",\"2690.87\"],[\"610420\",\"100\"],[\"610450\",\"610.86\"]," + + "[\"610500\",\"815.43\"],[\"610690\",\"25\"],[\"610700\",\"120.8\"]," + + "[\"610920\",\"524.38\"],[\"610930\",\"305.51\"],[\"611000\",\"937.44\"]," + + "[\"611060\",\"8.18\"],[\"611130\",\"69.91\"],[\"611140\",\"503\"]," + + "[\"611150\",\"601.79\"],[\"611190\",\"15\"],[\"611200\",\"1128.36\"]," + + "[\"611250\",\"2153.73\"],[\"611330\",\"500\"],[\"611360\",\"300\"]," + + "[\"611400\",\"21.5\"],[\"611500\",\"637.95\"],[\"611530\",\"2\"]]," + + "\"bids\":[[\"610320\",\"114.61\"],[\"610300\",\"491.56\"],[\"610290\",\"479\"]," + + "[\"610260\",\"474.2\"],[\"610240\",\"427.85\"],[\"610200\",\"183.67\"]," + + "[\"610160\",\"585.47\"],[\"610150\",\"396.31\"],[\"610140\",\"1615.92\"]," + + "[\"610120\",\"128.73\"],[\"610100\",\"5571.63\"],[\"610040\",\"6.84\"]," + + "[\"610000\",\"15505.56\"],[\"609930\",\"100\"],[\"609900\",\"46\"]," + + "[\"609810\",\"150\"],[\"609750\",\"196.76\"],[\"609730\",\"50.2\"]," + + "[\"609700\",\"411.97\"],[\"609650\",\"1640.12\"],[\"609640\",\"480.23\"]," + + "[\"609600\",\"410.04\"],[\"609560\",\"1640.36\"],[\"609530\",\"2.14\"]]," + + "\"lastTradePrice\":\"610350\",\"lastUpdate\":1727685711521}"; + + String delta3_str = "Fa\nQ:{\"asks\":[[\"610320\",\"312.757@4v,E:0350\",\"3418.837@6x,D:0380\",\"1743.78@8W," + + "68@r,A:],\"bids\":[6y@7g,I:,[\"609520\",\"143.57p@Eb,5:3315}3QQaIf;"; + + String out3 = "{\"asks\":[[\"610320\",\"312.75\"],[\"610350\",\"3418.83\"],[\"610380\",\"1743.7\"]," + + "[\"610400\",\"5133.73\"],[\"610410\",\"2690.87\"],[\"610420\",\"100\"]," + + "[\"610450\",\"610.86\"],[\"610500\",\"815.43\"],[\"610690\",\"25\"]," + + "[\"610700\",\"120.8\"],[\"610920\",\"524.38\"],[\"610930\",\"305.51\"]," + + "[\"611000\",\"937.44\"],[\"611060\",\"8.18\"],[\"611130\",\"69.91\"]," + + "[\"611140\",\"503\"],[\"611150\",\"601.79\"],[\"611190\",\"15\"]," + + "[\"611200\",\"1128.36\"],[\"611250\",\"2153.73\"],[\"611330\",\"500\"]," + + "[\"611360\",\"300\"],[\"611400\",\"21.5\"],[\"611500\",\"637.95\"]]," + + "\"bids\":[[\"610300\",\"491.56\"],[\"610290\",\"479\"],[\"610260\",\"474.2\"]," + + "[\"610240\",\"427.85\"],[\"610200\",\"183.67\"],[\"610160\",\"585.47\"]," + + "[\"610150\",\"396.31\"],[\"610140\",\"1615.92\"],[\"610120\",\"128.73\"]," + + "[\"610100\",\"5571.63\"],[\"610040\",\"6.84\"],[\"610000\",\"15505.56\"]," + + "[\"609930\",\"100\"],[\"609900\",\"46\"],[\"609810\",\"150\"]," + + "[\"609750\",\"196.76\"],[\"609730\",\"50.2\"],[\"609700\",\"411.97\"]," + + "[\"609650\",\"1640.12\"],[\"609640\",\"480.23\"],[\"609600\",\"410.04\"]," + + "[\"609560\",\"1640.36\"],[\"609530\",\"2.14\"],[\"609520\",\"143.57\"]]," + + "\"lastTradePrice\":\"610350\",\"lastUpdate\":1727685713315}"; + + result_bytes = Fossil.applyDelta( + source3_str.getBytes("UTF-8"), + delta3_str.getBytes("UTF-8") + ); + assertEquals(out3, new String(result_bytes, "UTF-8")); + + // Test 4 + String source4_str = "{\"asks\":[[\"610390\",\"600.45\"],[\"610400\",\"118.16\"],[\"610410\",\"2450.9\"]," + + "[\"610420\",\"100\"],[\"610450\",\"413.91\"],[\"610490\",\"20\"]," + + "[\"610500\",\"865.43\"],[\"610690\",\"25\"],[\"610700\",\"120.8\"]," + + "[\"610800\",\"325.49\"],[\"610900\",\"43\"],[\"610930\",\"386.35\"]," + + "[\"610980\",\"25\"],[\"610990\",\"1304.18\"],[\"611000\",\"6729.02\"]," + + "[\"611060\",\"8.18\"],[\"611140\",\"105\"],[\"611150\",\"601.79\"]," + + "[\"611190\",\"15\"],[\"611200\",\"1118.36\"],[\"611250\",\"2253.63\"]," + + "[\"611330\",\"500\"],[\"611350\",\"200.66\"],[\"611360\",\"300\"]]," + + "\"bids\":[[\"610260\",\"73.51\"],[\"610250\",\"1884.19\"],[\"610240\",\"27.79\"]," + + "[\"610230\",\"55.7\"],[\"610100\",\"88.94\"],[\"610060\",\"957.52\"]," + + "[\"610040\",\"48.84\"],[\"610000\",\"7344.08\"],[\"609990\",\"234.11\"]," + + "[\"609800\",\"2.1\"],[\"609720\",\"50\"],[\"609670\",\"2583.24\"]," + + "[\"609660\",\"50.99\"],[\"609650\",\"922.5\"],[\"609640\",\"381.02\"]," + + "[\"609600\",\"410.04\"],[\"609560\",\"1640.36\"],[\"609530\",\"2.14\"]," + + "[\"609520\",\"143.57\"],[\"609510\",\"531.75\"],[\"609500\",\"5885.67\"]," + + "[\"609460\",\"86.27\"],[\"609430\",\"100\"],[\"609400\",\"410.17\"]]," + + "\"lastTradePrice\":\"610390\",\"lastUpdate\":1727688337312}"; + + String delta4_str = "FO\nP@0,74@Q,5:68.377N@7Z,1:5P@Ew,5:9024}9u5zN;"; + + String out4 = "{\"asks\":[[\"610390\",\"600.4\"],[\"610400\",\"118.16\"],[\"610410\",\"2450.9\"]," + + "[\"610420\",\"100\"],[\"610450\",\"413.91\"],[\"610490\",\"20\"]," + + "[\"610500\",\"865.43\"],[\"610690\",\"25\"],[\"610700\",\"120.8\"]," + + "[\"610800\",\"325.49\"],[\"610900\",\"43\"],[\"610930\",\"386.35\"]," + + "[\"610980\",\"25\"],[\"610990\",\"1304.18\"],[\"611000\",\"6729.02\"]," + + "[\"611060\",\"8.18\"],[\"611140\",\"105\"],[\"611150\",\"601.79\"]," + + "[\"611190\",\"15\"],[\"611200\",\"1118.36\"],[\"611250\",\"2253.63\"]," + + "[\"611330\",\"500\"],[\"611350\",\"200.66\"],[\"611360\",\"300\"]]," + + "\"bids\":[[\"610260\",\"68.37\"],[\"610250\",\"1884.19\"],[\"610240\",\"27.79\"]," + + "[\"610230\",\"55.7\"],[\"610100\",\"88.94\"],[\"610060\",\"957.52\"]," + + "[\"610040\",\"48.84\"],[\"610000\",\"7344.08\"],[\"609990\",\"234.11\"]," + + "[\"609800\",\"2.1\"],[\"609720\",\"50\"],[\"609670\",\"2583.24\"]," + + "[\"609660\",\"50.99\"],[\"609650\",\"922.5\"],[\"609640\",\"381.02\"]," + + "[\"609600\",\"410.04\"],[\"609560\",\"1640.36\"],[\"609530\",\"2.14\"]," + + "[\"609520\",\"143.57\"],[\"609510\",\"531.75\"],[\"609500\",\"5885.67\"]," + + "[\"609460\",\"86.27\"],[\"609430\",\"100\"],[\"609400\",\"410.17\"]]," + + "\"lastTradePrice\":\"610350\",\"lastUpdate\":1727688339024}"; + + result_bytes = Fossil.applyDelta( + source4_str.getBytes("UTF-8"), + delta4_str.getBytes("UTF-8") + ); + assertEquals(out4, new String(result_bytes, "UTF-8")); + + // Test 5 + // source5_str is out4 + String source5_str = out4; + String delta5_str = "FP\nK@0,6:593.1775@P,4:0.517N@7Y,1:9Q@Ev,4:892}1bjQuR;"; + + String out5 = "{\"asks\":[[\"610390\",\"593.17\"],[\"610400\",\"118.16\"],[\"610410\",\"2450.9\"]," + + "[\"610420\",\"100\"],[\"610450\",\"413.91\"],[\"610490\",\"20\"]," + + "[\"610500\",\"865.43\"],[\"610690\",\"25\"],[\"610700\",\"120.8\"]," + + "[\"610800\",\"325.49\"],[\"610900\",\"43\"],[\"610930\",\"386.35\"]," + + "[\"610980\",\"25\"],[\"610990\",\"1304.18\"],[\"611000\",\"6729.02\"]," + + "[\"611060\",\"8.18\"],[\"611140\",\"105\"],[\"611150\",\"601.79\"]," + + "[\"611190\",\"15\"],[\"611200\",\"1118.36\"],[\"611250\",\"2253.63\"]," + + "[\"611330\",\"500\"],[\"611350\",\"200.66\"],[\"611360\",\"300\"]]," + + "\"bids\":[[\"610260\",\"60.51\"],[\"610250\",\"1884.19\"],[\"610240\",\"27.79\"]," + + "[\"610230\",\"55.7\"],[\"610100\",\"88.94\"],[\"610060\",\"957.52\"]," + + "[\"610040\",\"48.84\"],[\"610000\",\"7344.08\"],[\"609990\",\"234.11\"]," + + "[\"609800\",\"2.1\"],[\"609720\",\"50\"],[\"609670\",\"2583.24\"]," + + "[\"609660\",\"50.99\"],[\"609650\",\"922.5\"],[\"609640\",\"381.02\"]," + + "[\"609600\",\"410.04\"],[\"609560\",\"1640.36\"],[\"609530\",\"2.14\"]," + + "[\"609520\",\"143.57\"],[\"609510\",\"531.75\"],[\"609500\",\"5885.67\"]," + + "[\"609460\",\"86.27\"],[\"609430\",\"100\"],[\"609400\",\"410.17\"]]," + + "\"lastTradePrice\":\"610390\",\"lastUpdate\":1727688339892}"; + + result_bytes = Fossil.applyDelta( + source5_str.getBytes("UTF-8"), + delta5_str.getBytes("UTF-8") + ); + assertEquals(out5, new String(result_bytes, "UTF-8")); + } + + @Test + public void testApplyDeltaFromFiles() throws Exception { + for (int i = 1; i <= 5; i++) { + String dir = "data/" + i; + + byte[] origin = readResourceAsBytes(dir + "/origin"); + byte[] target = readResourceAsBytes(dir + "/target"); + byte[] delta = readResourceAsBytes(dir + "/delta"); + + // Apply the delta + byte[] applied = Fossil.applyDelta(origin, delta); + + // Assert that the applied result matches the target + assertArrayEquals("Test case " + i + " failed", target, applied); + } + } + + @Test + public void testApplyDeltaWithTruncatedDelta() throws Exception { + String dir = "data/1"; + + byte[] origin = readResourceAsBytes(dir + "/origin"); + byte[] delta = readResourceAsBytes(dir + "/delta"); + + // Truncate the delta by removing the last byte + byte[] truncatedDelta = new byte[delta.length - 1]; + System.arraycopy(delta, 0, truncatedDelta, 0, delta.length - 1); + + try { + // Attempt to apply the truncated delta + Fossil.applyDelta(origin, truncatedDelta); + // If no exception is thrown, the test should fail + fail("Expected an exception to be thrown due to truncated delta"); + } catch (Exception e) { + // Exception is expected + } + } + + /** + * Helper method to read a resource file into a byte array. + */ + private byte[] readResourceAsBytes(String resourcePath) throws Exception { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath); + if (inputStream == null) { + throw new Exception("Resource not found: " + resourcePath); + } + + try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { + byte[] data = new byte[16384]; // 16KB buffer + int nRead; + while ((nRead = inputStream.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + return buffer.toByteArray(); + } finally { + inputStream.close(); + } + } +} diff --git a/centrifuge/src/test/resources/data/1/delta b/centrifuge/src/test/resources/data/1/delta new file mode 100644 index 0000000..5b28e35 --- /dev/null +++ b/centrifuge/src/test/resources/data/1/delta @@ -0,0 +1,2 @@ +RR +C~@0,K:nineteen, with short5f@DI,5:Maste1h@J3,9:one month6_@Ku,gWXkC; \ No newline at end of file diff --git a/centrifuge/src/test/resources/data/1/origin b/centrifuge/src/test/resources/data/1/origin new file mode 100644 index 0000000..70db0d9 --- /dev/null +++ b/centrifuge/src/test/resources/data/1/origin @@ -0,0 +1,9 @@ +On the 15th of September, 1840, about six o'clock in the morning, the Ville de Montereau, just on the point of starting, was sending forth great whirlwinds of smoke, in front of the Quai St. Bernard. + +People came rushing on board in breathless haste. The traffic was obstructed by casks, cables, and baskets of linen. The sailors answered nobody. People jostled one another. Between the two paddle-boxes was piled up a heap of parcels; and the uproar was drowned in the loud hissing of the steam, which, making its way through the plates of sheet-iron, enveloped everything in a white cloud, while the bell at the prow kept ringing continuously. + +At last, the vessel set out; and the two banks of the river, stocked with warehouses, timber-yards, and manufactories, opened out like two huge ribbons being unrolled. + +A young man of eighteen, with long hair, holding an album under his arm, remained near the helm without moving. Through the haze he surveyed steeples, buildings of which he did not know the names; then, with a parting glance, he took in the Île St. Louis, the Cité, Nôtre Dame; and presently, as Paris disappeared from his view, he heaved a deep sigh. + +Frederick Moreau, having just taken his Bachelor's degree, was returning home to Nogent-sur-Seine, where he would have to lead a languishing existence for two months, before going back to begin his legal studies. His mother had sent him, with enough to cover his expenses, to Havre to see an uncle, from whom she had expectations of his receiving an inheritance. He had returned from that place only yesterday; and he indemnified himself for not having the opportunity of spending a little time in the capital by taking the longest possible route to reach his own part of the country. diff --git a/centrifuge/src/test/resources/data/1/target b/centrifuge/src/test/resources/data/1/target new file mode 100644 index 0000000..8fe0f32 --- /dev/null +++ b/centrifuge/src/test/resources/data/1/target @@ -0,0 +1,9 @@ +On the 15th of September, 1840, about six o'clock in the morning, the Ville de Montereau, just on the point of starting, was sending forth great whirlwinds of smoke, in front of the Quai St. Bernard. + +People came rushing on board in breathless haste. The traffic was obstructed by casks, cables, and baskets of linen. The sailors answered nobody. People jostled one another. Between the two paddle-boxes was piled up a heap of parcels; and the uproar was drowned in the loud hissing of the steam, which, making its way through the plates of sheet-iron, enveloped everything in a white cloud, while the bell at the prow kept ringing continuously. + +At last, the vessel set out; and the two banks of the river, stocked with warehouses, timber-yards, and manufactories, opened out like two huge ribbons being unrolled. + +A young man of nineteen, with short hair, holding an album under his arm, remained near the helm without moving. Through the haze he surveyed steeples, buildings of which he did not know the names; then, with a parting glance, he took in the Île St. Louis, the Cité, Nôtre Dame; and presently, as Paris disappeared from his view, he heaved a deep sigh. + +Frederick Moreau, having just taken his Master's degree, was returning home to Nogent-sur-Seine, where he would have to lead a languishing existence for one month, before going back to begin his legal studies. His mother had sent him, with enough to cover his expenses, to Havre to see an uncle, from whom she had expectations of his receiving an inheritance. He had returned from that place only yesterday; and he indemnified himself for not having the opportunity of spending a little time in the capital by taking the longest possible route to reach his own part of the country. diff --git a/centrifuge/src/test/resources/data/2/delta b/centrifuge/src/test/resources/data/2/delta new file mode 100644 index 0000000..dc0d571 Binary files /dev/null and b/centrifuge/src/test/resources/data/2/delta differ diff --git a/centrifuge/src/test/resources/data/2/origin b/centrifuge/src/test/resources/data/2/origin new file mode 100644 index 0000000..956c49f Binary files /dev/null and b/centrifuge/src/test/resources/data/2/origin differ diff --git a/centrifuge/src/test/resources/data/2/target b/centrifuge/src/test/resources/data/2/target new file mode 100644 index 0000000..1cf6b78 Binary files /dev/null and b/centrifuge/src/test/resources/data/2/target differ diff --git a/centrifuge/src/test/resources/data/3/delta b/centrifuge/src/test/resources/data/3/delta new file mode 100644 index 0000000..82cf653 --- /dev/null +++ b/centrifuge/src/test/resources/data/3/delta @@ -0,0 +1,15 @@ +EB +EB:"The man was seen," continued Lestrade. "A milk boy, passing on his way +to the dairy, happened to walk down the lane which leads from the mews +at the back of the hotel. He noticed that a ladder, which usually lay +there, was raised against one of the windows of the second floor, which +was wide open. After passing, he looked back and saw a man descend the +ladder. He came down so quietly and openly that the boy imagined him to +be some carpenter or joiner at work in the hotel. He took no particular +notice of him, beyond thinking in his own mind that it was early for him +to be at work. He has an impression that the man was tall, had a reddish +face, and was dressed in a long, brownish coat. He must have stayed in +the room some little time after the murder, for we found blood-stained +water in the basin, where he had washed his hands, and marks on the +sheets where he had deliberately wiped his knife." +23cQLK; \ No newline at end of file diff --git a/centrifuge/src/test/resources/data/3/origin b/centrifuge/src/test/resources/data/3/origin new file mode 100644 index 0000000..a77704a --- /dev/null +++ b/centrifuge/src/test/resources/data/3/origin @@ -0,0 +1,19 @@ +"MY DEAR MR. SHERLOCK HOLMES,-- + +"There has been a bad business during the night at 3, Lauriston Gardens, +off the Brixton Road. Our man on the beat saw a light there about two in +the morning, and as the house was an empty one, suspected that something +was amiss. He found the door open, and in the front room, which is bare +of furniture, discovered the body of a gentleman, well dressed, and +having cards in his pocket bearing the name of 'Enoch J. Drebber, +Cleveland, Ohio, U.S.A.' There had been no robbery, nor is there any +evidence as to how the man met his death. There are marks of blood in +the room, but there is no wound upon his person. We are at a loss as to +how he came into the empty house; indeed, the whole affair is a puzzler. +If you can come round to the house any time before twelve, you will find +me there. I have left everything _in statu quo_ until I hear from you. +If you are unable to come I shall give you fuller details, and would +esteem it a great kindness if you would favour me with your opinion. +Yours faithfully, + +"TOBIAS GREGSON." diff --git a/centrifuge/src/test/resources/data/3/target b/centrifuge/src/test/resources/data/3/target new file mode 100644 index 0000000..86a42c8 --- /dev/null +++ b/centrifuge/src/test/resources/data/3/target @@ -0,0 +1,13 @@ +"The man was seen," continued Lestrade. "A milk boy, passing on his way +to the dairy, happened to walk down the lane which leads from the mews +at the back of the hotel. He noticed that a ladder, which usually lay +there, was raised against one of the windows of the second floor, which +was wide open. After passing, he looked back and saw a man descend the +ladder. He came down so quietly and openly that the boy imagined him to +be some carpenter or joiner at work in the hotel. He took no particular +notice of him, beyond thinking in his own mind that it was early for him +to be at work. He has an impression that the man was tall, had a reddish +face, and was dressed in a long, brownish coat. He must have stayed in +the room some little time after the murder, for we found blood-stained +water in the basin, where he had washed his hands, and marks on the +sheets where he had deliberately wiped his knife." diff --git a/centrifuge/src/test/resources/data/4/delta b/centrifuge/src/test/resources/data/4/delta new file mode 100644 index 0000000..2896aff --- /dev/null +++ b/centrifuge/src/test/resources/data/4/delta @@ -0,0 +1,3 @@ +x +j@0,E:First note"}} +5NpHA; \ No newline at end of file diff --git a/centrifuge/src/test/resources/data/4/origin b/centrifuge/src/test/resources/data/4/origin new file mode 100644 index 0000000..7aa38a6 --- /dev/null +++ b/centrifuge/src/test/resources/data/4/origin @@ -0,0 +1 @@ +{"42182fb2426340ac5fad4e511d8a2dc4":{"title":"New note"}} diff --git a/centrifuge/src/test/resources/data/4/target b/centrifuge/src/test/resources/data/4/target new file mode 100644 index 0000000..71f234a --- /dev/null +++ b/centrifuge/src/test/resources/data/4/target @@ -0,0 +1 @@ +{"42182fb2426340ac5fad4e511d8a2dc4":{"title":"First note"}} diff --git a/centrifuge/src/test/resources/data/5/delta b/centrifuge/src/test/resources/data/5/delta new file mode 100644 index 0000000..b74b685 --- /dev/null +++ b/centrifuge/src/test/resources/data/5/delta @@ -0,0 +1,2 @@ +6 +6:little3OocHp; \ No newline at end of file diff --git a/centrifuge/src/test/resources/data/5/origin b/centrifuge/src/test/resources/data/5/origin new file mode 100644 index 0000000..18a34fb --- /dev/null +++ b/centrifuge/src/test/resources/data/5/origin @@ -0,0 +1 @@ +tiny \ No newline at end of file diff --git a/centrifuge/src/test/resources/data/5/target b/centrifuge/src/test/resources/data/5/target new file mode 100644 index 0000000..a06f92f --- /dev/null +++ b/centrifuge/src/test/resources/data/5/target @@ -0,0 +1 @@ +little \ No newline at end of file diff --git a/example/src/main/java/io/github/centrifugal/centrifuge/example/Main.java b/example/src/main/java/io/github/centrifugal/centrifuge/example/Main.java index 7f330f1..95b6838 100644 --- a/example/src/main/java/io/github/centrifugal/centrifuge/example/Main.java +++ b/example/src/main/java/io/github/centrifugal/centrifuge/example/Main.java @@ -150,8 +150,11 @@ public void onLeave(Subscription sub, LeaveEvent event) { }; Subscription sub; + SubscriptionOptions subOpts = new SubscriptionOptions(); + // You can set `delta` to `"fossil"` for using delta compression via + // `subOpts.setDelta("fossil")`; try { - sub = client.newSubscription("chat:index", new SubscriptionOptions(), subListener); + sub = client.newSubscription("chat:index", subOpts, subListener); } catch (DuplicateSubscriptionException e) { e.printStackTrace(); return;