@automerge/automerge-repo-network-websocket 1.0.19 → 1.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BrowserWebSocketClientAdapter.d.ts +3 -3
- package/dist/BrowserWebSocketClientAdapter.d.ts.map +1 -1
- package/dist/BrowserWebSocketClientAdapter.js +17 -11
- package/dist/NodeWSServerAdapter.d.ts +2 -2
- package/dist/NodeWSServerAdapter.d.ts.map +1 -1
- package/dist/NodeWSServerAdapter.js +40 -29
- package/dist/messages.d.ts +11 -1
- package/dist/messages.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/BrowserWebSocketClientAdapter.ts +38 -11
- package/src/NodeWSServerAdapter.ts +47 -31
- package/src/messages.ts +17 -1
- package/test/Websocket.test.ts +95 -54
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="ws" />
|
|
2
|
-
import { NetworkAdapter, PeerId } from "@automerge/automerge-repo";
|
|
2
|
+
import { NetworkAdapter, type PeerId, type StorageId } from "@automerge/automerge-repo";
|
|
3
3
|
import WebSocket from "isomorphic-ws";
|
|
4
4
|
import { FromClientMessage } from "./messages.js";
|
|
5
5
|
declare abstract class WebSocketNetworkAdapter extends NetworkAdapter {
|
|
@@ -11,14 +11,14 @@ export declare class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapt
|
|
|
11
11
|
remotePeerId?: PeerId;
|
|
12
12
|
url: string;
|
|
13
13
|
constructor(url: string);
|
|
14
|
-
connect(peerId: PeerId): void;
|
|
14
|
+
connect(peerId: PeerId, storageId: StorageId | undefined, isEphemeral: boolean): void;
|
|
15
15
|
onOpen: () => void;
|
|
16
16
|
onClose: () => void;
|
|
17
17
|
onMessage: (event: WebSocket.MessageEvent) => void;
|
|
18
18
|
join(): void;
|
|
19
19
|
disconnect(): void;
|
|
20
20
|
send(message: FromClientMessage): void;
|
|
21
|
-
announceConnection(peerId: PeerId): void;
|
|
21
|
+
announceConnection(peerId: PeerId, storageId: StorageId | undefined, isEphemeral: boolean): void;
|
|
22
22
|
receiveMessage(message: Uint8Array): void;
|
|
23
23
|
}
|
|
24
24
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrowserWebSocketClientAdapter.d.ts","sourceRoot":"","sources":["../src/BrowserWebSocketClientAdapter.ts"],"names":[],"mappings":";AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"BrowserWebSocketClientAdapter.d.ts","sourceRoot":"","sources":["../src/BrowserWebSocketClientAdapter.ts"],"names":[],"mappings":";AAAA,OAAO,EACL,cAAc,EACd,KAAK,MAAM,EAEX,KAAK,SAAS,EACf,MAAM,2BAA2B,CAAA;AAClC,OAAO,SAAS,MAAM,eAAe,CAAA;AAIrC,OAAO,EACL,iBAAiB,EAIlB,MAAM,eAAe,CAAA;AAKtB,uBAAe,uBAAwB,SAAQ,cAAc;IAC3D,MAAM,CAAC,EAAE,SAAS,CAAA;CACnB;AAED,qBAAa,6BAA8B,SAAQ,uBAAuB;;IAGxE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;IACvC,YAAY,CAAC,EAAE,MAAM,CAAA;IAGrB,GAAG,EAAE,MAAM,CAAA;gBAEC,GAAG,EAAE,MAAM;IAKvB,OAAO,CACL,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,WAAW,EAAE,OAAO;IAwCtB,MAAM,aAKL;IAGD,OAAO,aAYN;IAED,SAAS,UAAW,sBAAsB,UAEzC;IAED,IAAI;IAWJ,UAAU;IAOV,IAAI,CAAC,OAAO,EAAE,iBAAiB;IAwB/B,kBAAkB,CAChB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,WAAW,EAAE,OAAO;IAetB,cAAc,CAAC,OAAO,EAAE,UAAU;CA4BnC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NetworkAdapter, cbor } from "@automerge/automerge-repo";
|
|
1
|
+
import { NetworkAdapter, cbor, } from "@automerge/automerge-repo";
|
|
2
2
|
import WebSocket from "isomorphic-ws";
|
|
3
3
|
import debug from "debug";
|
|
4
4
|
import { ProtocolV1 } from "./protocolVersion.js";
|
|
@@ -17,7 +17,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
17
17
|
super();
|
|
18
18
|
this.url = url;
|
|
19
19
|
}
|
|
20
|
-
connect(peerId) {
|
|
20
|
+
connect(peerId, storageId, isEphemeral) {
|
|
21
21
|
// If we're reconnecting make sure we remove the old event listeners
|
|
22
22
|
// before creating a new connection.
|
|
23
23
|
if (this.socket) {
|
|
@@ -26,9 +26,11 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
26
26
|
this.socket.removeEventListener("message", this.onMessage);
|
|
27
27
|
}
|
|
28
28
|
if (!this.timerId) {
|
|
29
|
-
this.timerId = setInterval(() => this.connect(peerId), 5000);
|
|
29
|
+
this.timerId = setInterval(() => this.connect(peerId, storageId, isEphemeral), 5000);
|
|
30
30
|
}
|
|
31
31
|
this.peerId = peerId;
|
|
32
|
+
this.storageId = storageId;
|
|
33
|
+
this.isEphemeral = isEphemeral;
|
|
32
34
|
this.socket = new WebSocket(this.url);
|
|
33
35
|
this.socket.binaryType = "arraybuffer";
|
|
34
36
|
this.socket.addEventListener("open", this.onOpen);
|
|
@@ -49,7 +51,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
49
51
|
log(`@ ${this.url}: open`);
|
|
50
52
|
clearInterval(this.timerId);
|
|
51
53
|
this.timerId = undefined;
|
|
52
|
-
this.send(joinMessage(this.peerId));
|
|
54
|
+
this.send(joinMessage(this.peerId, this.storageId, this.isEphemeral));
|
|
53
55
|
};
|
|
54
56
|
// When a socket closes, or disconnects, remove it from the array.
|
|
55
57
|
onClose = () => {
|
|
@@ -59,7 +61,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
59
61
|
}
|
|
60
62
|
if (!this.timerId) {
|
|
61
63
|
if (this.peerId) {
|
|
62
|
-
this.connect(this.peerId);
|
|
64
|
+
this.connect(this.peerId, this.storageId, this.isEphemeral);
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
};
|
|
@@ -71,7 +73,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
71
73
|
throw new Error("WTF, get a socket");
|
|
72
74
|
}
|
|
73
75
|
if (this.socket.readyState === WebSocket.OPEN) {
|
|
74
|
-
this.send(joinMessage(this.peerId));
|
|
76
|
+
this.send(joinMessage(this.peerId, this.storageId, this.isEphemeral));
|
|
75
77
|
}
|
|
76
78
|
else {
|
|
77
79
|
// The onOpen handler automatically sends a join message
|
|
@@ -99,7 +101,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
99
101
|
const arrayBuf = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
|
|
100
102
|
this.socket?.send(arrayBuf);
|
|
101
103
|
}
|
|
102
|
-
announceConnection(peerId) {
|
|
104
|
+
announceConnection(peerId, storageId, isEphemeral) {
|
|
103
105
|
// return a peer object
|
|
104
106
|
const myPeerId = this.peerId;
|
|
105
107
|
if (!myPeerId) {
|
|
@@ -110,7 +112,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
110
112
|
this.emit("ready", { network: this });
|
|
111
113
|
}
|
|
112
114
|
this.remotePeerId = peerId;
|
|
113
|
-
this.emit("peer-candidate", { peerId });
|
|
115
|
+
this.emit("peer-candidate", { peerId, storageId, isEphemeral });
|
|
114
116
|
}
|
|
115
117
|
receiveMessage(message) {
|
|
116
118
|
const decoded = cbor.decode(new Uint8Array(message));
|
|
@@ -123,10 +125,12 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
123
125
|
throw new Error("received a zero-length message");
|
|
124
126
|
}
|
|
125
127
|
switch (type) {
|
|
126
|
-
case "peer":
|
|
128
|
+
case "peer": {
|
|
129
|
+
const { storageId, isEphemeral } = decoded;
|
|
127
130
|
log(`peer: ${senderId}`);
|
|
128
|
-
this.announceConnection(senderId);
|
|
131
|
+
this.announceConnection(senderId, storageId, isEphemeral);
|
|
129
132
|
break;
|
|
133
|
+
}
|
|
130
134
|
case "error":
|
|
131
135
|
log(`error: ${decoded.message}`);
|
|
132
136
|
break;
|
|
@@ -135,10 +139,12 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
135
139
|
}
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
|
-
function joinMessage(senderId) {
|
|
142
|
+
function joinMessage(senderId, storageId, isEphemeral) {
|
|
139
143
|
return {
|
|
140
144
|
type: "join",
|
|
141
145
|
senderId,
|
|
146
|
+
storageId,
|
|
147
|
+
isEphemeral,
|
|
142
148
|
supportedProtocolVersions: [ProtocolV1],
|
|
143
149
|
};
|
|
144
150
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="ws" />
|
|
2
2
|
import WebSocket from "isomorphic-ws";
|
|
3
3
|
import { type WebSocketServer } from "isomorphic-ws";
|
|
4
|
-
import { NetworkAdapter, type PeerId } from "@automerge/automerge-repo";
|
|
4
|
+
import { NetworkAdapter, type PeerId, type StorageId } from "@automerge/automerge-repo";
|
|
5
5
|
import { FromServerMessage } from "./messages.js";
|
|
6
6
|
export declare class NodeWSServerAdapter extends NetworkAdapter {
|
|
7
7
|
server: WebSocketServer;
|
|
@@ -9,7 +9,7 @@ export declare class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
9
9
|
[peerId: PeerId]: WebSocket;
|
|
10
10
|
};
|
|
11
11
|
constructor(server: WebSocketServer);
|
|
12
|
-
connect(peerId: PeerId): void;
|
|
12
|
+
connect(peerId: PeerId, storageId: StorageId | undefined, isEphemeral: boolean): void;
|
|
13
13
|
disconnect(): void;
|
|
14
14
|
send(message: FromServerMessage): void;
|
|
15
15
|
receiveMessage(message: Uint8Array, socket: WebSocket): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeWSServerAdapter.d.ts","sourceRoot":"","sources":["../src/NodeWSServerAdapter.ts"],"names":[],"mappings":";AAAA,OAAO,SAAS,MAAM,eAAe,CAAA;AACrC,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAKpD,OAAO,EAEL,cAAc,EACd,KAAK,MAAM,
|
|
1
|
+
{"version":3,"file":"NodeWSServerAdapter.d.ts","sourceRoot":"","sources":["../src/NodeWSServerAdapter.ts"],"names":[],"mappings":";AAAA,OAAO,SAAS,MAAM,eAAe,CAAA;AACrC,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAKpD,OAAO,EAEL,cAAc,EACd,KAAK,MAAM,EACX,KAAK,SAAS,EACf,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAqB,iBAAiB,EAAE,MAAM,eAAe,CAAA;AASpE,qBAAa,mBAAoB,SAAQ,cAAc;IACrD,MAAM,EAAE,eAAe,CAAA;IACvB,OAAO,EAAE;QAAE,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAK;gBAEjC,MAAM,EAAE,eAAe;IAKnC,OAAO,CACL,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,WAAW,EAAE,OAAO;IAuDtB,UAAU;IAIV,IAAI,CAAC,OAAO,EAAE,iBAAiB;IAyB/B,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS;CAwEtD"}
|
|
@@ -11,8 +11,10 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
11
11
|
super();
|
|
12
12
|
this.server = server;
|
|
13
13
|
}
|
|
14
|
-
connect(peerId) {
|
|
14
|
+
connect(peerId, storageId, isEphemeral) {
|
|
15
15
|
this.peerId = peerId;
|
|
16
|
+
this.storageId = storageId;
|
|
17
|
+
this.isEphemeral = isEphemeral;
|
|
16
18
|
this.server.on("close", function close() {
|
|
17
19
|
clearInterval(interval);
|
|
18
20
|
});
|
|
@@ -84,36 +86,45 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
84
86
|
log(`[${senderId}->${myPeerId}${"documentId" in cbor ? "@" + cbor.documentId : ""}] ${type} | ${message.byteLength} bytes`);
|
|
85
87
|
switch (type) {
|
|
86
88
|
case "join":
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (existingSocket
|
|
90
|
-
existingSocket.
|
|
89
|
+
{
|
|
90
|
+
const existingSocket = this.sockets[senderId];
|
|
91
|
+
if (existingSocket) {
|
|
92
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
93
|
+
existingSocket.close();
|
|
94
|
+
}
|
|
95
|
+
this.emit("peer-disconnected", { peerId: senderId });
|
|
91
96
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// (and we pretend to be joined to every channel)
|
|
99
|
-
const selectedProtocolVersion = selectProtocol(cbor.supportedProtocolVersions);
|
|
100
|
-
if (selectedProtocolVersion === null) {
|
|
101
|
-
this.send({
|
|
102
|
-
type: "error",
|
|
103
|
-
senderId: this.peerId,
|
|
104
|
-
message: "unsupported protocol version",
|
|
105
|
-
targetId: senderId,
|
|
106
|
-
});
|
|
107
|
-
this.sockets[senderId].close();
|
|
108
|
-
delete this.sockets[senderId];
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
this.send({
|
|
112
|
-
type: "peer",
|
|
113
|
-
senderId: this.peerId,
|
|
114
|
-
selectedProtocolVersion: ProtocolV1,
|
|
115
|
-
targetId: senderId,
|
|
97
|
+
const { storageId, isEphemeral } = cbor;
|
|
98
|
+
// Let the rest of the system know that we have a new connection.
|
|
99
|
+
this.emit("peer-candidate", {
|
|
100
|
+
peerId: senderId,
|
|
101
|
+
storageId,
|
|
102
|
+
isEphemeral,
|
|
116
103
|
});
|
|
104
|
+
this.sockets[senderId] = socket;
|
|
105
|
+
// In this client-server connection, there's only ever one peer: us!
|
|
106
|
+
// (and we pretend to be joined to every channel)
|
|
107
|
+
const selectedProtocolVersion = selectProtocol(cbor.supportedProtocolVersions);
|
|
108
|
+
if (selectedProtocolVersion === null) {
|
|
109
|
+
this.send({
|
|
110
|
+
type: "error",
|
|
111
|
+
senderId: this.peerId,
|
|
112
|
+
message: "unsupported protocol version",
|
|
113
|
+
targetId: senderId,
|
|
114
|
+
});
|
|
115
|
+
this.sockets[senderId].close();
|
|
116
|
+
delete this.sockets[senderId];
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.send({
|
|
120
|
+
type: "peer",
|
|
121
|
+
senderId: this.peerId,
|
|
122
|
+
storageId: this.storageId,
|
|
123
|
+
isEphemeral: this.isEphemeral,
|
|
124
|
+
selectedProtocolVersion: ProtocolV1,
|
|
125
|
+
targetId: senderId,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
117
128
|
}
|
|
118
129
|
break;
|
|
119
130
|
case "leave":
|
package/dist/messages.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Message, PeerId } from "@automerge/automerge-repo";
|
|
1
|
+
import type { Message, PeerId, StorageId } from "@automerge/automerge-repo";
|
|
2
2
|
import type { ProtocolVersion } from "./protocolVersion.js";
|
|
3
3
|
/** The sender is disconnecting */
|
|
4
4
|
export type LeaveMessage = {
|
|
@@ -10,6 +10,11 @@ export type JoinMessage = {
|
|
|
10
10
|
type: "join";
|
|
11
11
|
/** The PeerID of the client */
|
|
12
12
|
senderId: PeerId;
|
|
13
|
+
/** Unique ID of the storage that the sender peer is using, is persistent across sessions */
|
|
14
|
+
storageId?: StorageId;
|
|
15
|
+
/** Indicates whether other peers should persist the sync state of the sender peer.
|
|
16
|
+
* Sync state is only persisted for non-ephemeral peers */
|
|
17
|
+
isEphemeral: boolean;
|
|
13
18
|
/** The protocol version the client supports */
|
|
14
19
|
supportedProtocolVersions: ProtocolVersion[];
|
|
15
20
|
};
|
|
@@ -18,6 +23,11 @@ export type PeerMessage = {
|
|
|
18
23
|
type: "peer";
|
|
19
24
|
/** The PeerID of the server */
|
|
20
25
|
senderId: PeerId;
|
|
26
|
+
/** Unique ID of the storage that the sender peer is using, is persistent across sessions */
|
|
27
|
+
storageId?: StorageId;
|
|
28
|
+
/** Indicates whether other peers should persist the sync state of the sender peer.
|
|
29
|
+
* Sync state is only persisted for non-ephemeral peers */
|
|
30
|
+
isEphemeral: boolean;
|
|
21
31
|
/** The protocol version the server selected for this connection */
|
|
22
32
|
selectedProtocolVersion: ProtocolVersion;
|
|
23
33
|
/** The PeerID of the client */
|
package/dist/messages.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAE3D,kCAAkC;AAClC,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,6EAA6E;AAC7E,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAEhB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,SAAS,CAAA;IAErB;8DAC0D;IAC1D,WAAW,EAAE,OAAO,CAAA;IAEpB,+CAA+C;IAC/C,yBAAyB,EAAE,eAAe,EAAE,CAAA;CAC7C,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAEhB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,SAAS,CAAA;IAErB;8DAC0D;IAC1D,WAAW,EAAE,OAAO,CAAA;IAEpB,mEAAmE;IACnE,uBAAuB,EAAE,eAAe,CAAA;IACxC,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,gGAAgG;AAChG,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,mCAAmC;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAKD,8CAA8C;AAC9C,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAA;AAEpE,8CAA8C;AAC9C,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automerge/automerge-repo-network-websocket",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.1.0-alpha.1",
|
|
4
4
|
"description": "isomorphic node/browser Websocket network adapter for Automerge Repo",
|
|
5
5
|
"repository": "https://github.com/automerge/automerge-repo/tree/master/packages/automerge-repo-network-websocket",
|
|
6
6
|
"author": "Peter van Hardenberg <pvh@pvh.ca>",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"test": "vitest"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@automerge/automerge-repo": "^1.0.
|
|
16
|
+
"@automerge/automerge-repo": "^1.1.0-alpha.1",
|
|
17
17
|
"cbor-x": "^1.3.0",
|
|
18
18
|
"eventemitter3": "^5.0.1",
|
|
19
19
|
"isomorphic-ws": "^5.0.0",
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
"publishConfig": {
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "11805d698f860bd6ffb3ca028d3b57e718690b5a"
|
|
34
34
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
NetworkAdapter,
|
|
3
|
+
type PeerId,
|
|
4
|
+
cbor,
|
|
5
|
+
type StorageId,
|
|
6
|
+
} from "@automerge/automerge-repo"
|
|
2
7
|
import WebSocket from "isomorphic-ws"
|
|
3
8
|
|
|
4
9
|
import debug from "debug"
|
|
@@ -7,6 +12,7 @@ import {
|
|
|
7
12
|
FromClientMessage,
|
|
8
13
|
FromServerMessage,
|
|
9
14
|
JoinMessage,
|
|
15
|
+
PeerMessage,
|
|
10
16
|
} from "./messages.js"
|
|
11
17
|
import { ProtocolV1 } from "./protocolVersion.js"
|
|
12
18
|
|
|
@@ -30,7 +36,11 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
30
36
|
this.url = url
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
connect(
|
|
39
|
+
connect(
|
|
40
|
+
peerId: PeerId,
|
|
41
|
+
storageId: StorageId | undefined,
|
|
42
|
+
isEphemeral: boolean
|
|
43
|
+
) {
|
|
34
44
|
// If we're reconnecting make sure we remove the old event listeners
|
|
35
45
|
// before creating a new connection.
|
|
36
46
|
if (this.socket) {
|
|
@@ -40,10 +50,15 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
if (!this.timerId) {
|
|
43
|
-
this.timerId = setInterval(
|
|
53
|
+
this.timerId = setInterval(
|
|
54
|
+
() => this.connect(peerId, storageId, isEphemeral),
|
|
55
|
+
5000
|
|
56
|
+
)
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
this.peerId = peerId
|
|
60
|
+
this.storageId = storageId
|
|
61
|
+
this.isEphemeral = isEphemeral
|
|
47
62
|
this.socket = new WebSocket(this.url)
|
|
48
63
|
this.socket.binaryType = "arraybuffer"
|
|
49
64
|
|
|
@@ -68,7 +83,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
68
83
|
log(`@ ${this.url}: open`)
|
|
69
84
|
clearInterval(this.timerId)
|
|
70
85
|
this.timerId = undefined
|
|
71
|
-
this.send(joinMessage(this.peerId
|
|
86
|
+
this.send(joinMessage(this.peerId!, this.storageId, this.isEphemeral))
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
// When a socket closes, or disconnects, remove it from the array.
|
|
@@ -81,7 +96,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
81
96
|
|
|
82
97
|
if (!this.timerId) {
|
|
83
98
|
if (this.peerId) {
|
|
84
|
-
this.connect(this.peerId)
|
|
99
|
+
this.connect(this.peerId, this.storageId, this.isEphemeral)
|
|
85
100
|
}
|
|
86
101
|
}
|
|
87
102
|
}
|
|
@@ -95,7 +110,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
95
110
|
throw new Error("WTF, get a socket")
|
|
96
111
|
}
|
|
97
112
|
if (this.socket.readyState === WebSocket.OPEN) {
|
|
98
|
-
this.send(joinMessage(this.peerId
|
|
113
|
+
this.send(joinMessage(this.peerId!, this.storageId, this.isEphemeral))
|
|
99
114
|
} else {
|
|
100
115
|
// The onOpen handler automatically sends a join message
|
|
101
116
|
}
|
|
@@ -132,7 +147,11 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
132
147
|
this.socket?.send(arrayBuf)
|
|
133
148
|
}
|
|
134
149
|
|
|
135
|
-
announceConnection(
|
|
150
|
+
announceConnection(
|
|
151
|
+
peerId: PeerId,
|
|
152
|
+
storageId: StorageId | undefined,
|
|
153
|
+
isEphemeral: boolean
|
|
154
|
+
) {
|
|
136
155
|
// return a peer object
|
|
137
156
|
const myPeerId = this.peerId
|
|
138
157
|
if (!myPeerId) {
|
|
@@ -143,7 +162,7 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
143
162
|
this.emit("ready", { network: this })
|
|
144
163
|
}
|
|
145
164
|
this.remotePeerId = peerId
|
|
146
|
-
this.emit("peer-candidate", { peerId })
|
|
165
|
+
this.emit("peer-candidate", { peerId, storageId, isEphemeral })
|
|
147
166
|
}
|
|
148
167
|
|
|
149
168
|
receiveMessage(message: Uint8Array) {
|
|
@@ -161,10 +180,12 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
161
180
|
}
|
|
162
181
|
|
|
163
182
|
switch (type) {
|
|
164
|
-
case "peer":
|
|
183
|
+
case "peer": {
|
|
184
|
+
const { storageId, isEphemeral } = decoded
|
|
165
185
|
log(`peer: ${senderId}`)
|
|
166
|
-
this.announceConnection(senderId)
|
|
186
|
+
this.announceConnection(senderId, storageId, isEphemeral)
|
|
167
187
|
break
|
|
188
|
+
}
|
|
168
189
|
case "error":
|
|
169
190
|
log(`error: ${decoded.message}`)
|
|
170
191
|
break
|
|
@@ -174,10 +195,16 @@ export class BrowserWebSocketClientAdapter extends WebSocketNetworkAdapter {
|
|
|
174
195
|
}
|
|
175
196
|
}
|
|
176
197
|
|
|
177
|
-
function joinMessage(
|
|
198
|
+
function joinMessage(
|
|
199
|
+
senderId: PeerId,
|
|
200
|
+
storageId: StorageId | undefined,
|
|
201
|
+
isEphemeral: boolean
|
|
202
|
+
): JoinMessage {
|
|
178
203
|
return {
|
|
179
204
|
type: "join",
|
|
180
205
|
senderId,
|
|
206
|
+
storageId,
|
|
207
|
+
isEphemeral,
|
|
181
208
|
supportedProtocolVersions: [ProtocolV1],
|
|
182
209
|
}
|
|
183
210
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
cbor as cborHelpers,
|
|
9
9
|
NetworkAdapter,
|
|
10
10
|
type PeerId,
|
|
11
|
+
type StorageId,
|
|
11
12
|
} from "@automerge/automerge-repo"
|
|
12
13
|
import { FromClientMessage, FromServerMessage } from "./messages.js"
|
|
13
14
|
import { ProtocolV1, ProtocolVersion } from "./protocolVersion.js"
|
|
@@ -27,8 +28,14 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
27
28
|
this.server = server
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
connect(
|
|
31
|
+
connect(
|
|
32
|
+
peerId: PeerId,
|
|
33
|
+
storageId: StorageId | undefined,
|
|
34
|
+
isEphemeral: boolean
|
|
35
|
+
) {
|
|
31
36
|
this.peerId = peerId
|
|
37
|
+
this.storageId = storageId
|
|
38
|
+
this.isEphemeral = isEphemeral
|
|
32
39
|
|
|
33
40
|
this.server.on("close", function close() {
|
|
34
41
|
clearInterval(interval)
|
|
@@ -124,39 +131,48 @@ export class NodeWSServerAdapter extends NetworkAdapter {
|
|
|
124
131
|
)
|
|
125
132
|
switch (type) {
|
|
126
133
|
case "join":
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (existingSocket
|
|
130
|
-
existingSocket.
|
|
134
|
+
{
|
|
135
|
+
const existingSocket = this.sockets[senderId]
|
|
136
|
+
if (existingSocket) {
|
|
137
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
138
|
+
existingSocket.close()
|
|
139
|
+
}
|
|
140
|
+
this.emit("peer-disconnected", { peerId: senderId })
|
|
131
141
|
}
|
|
132
|
-
this.emit("peer-disconnected", {peerId: senderId})
|
|
133
|
-
}
|
|
134
142
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const selectedProtocolVersion = selectProtocol(
|
|
142
|
-
cbor.supportedProtocolVersions
|
|
143
|
-
)
|
|
144
|
-
if (selectedProtocolVersion === null) {
|
|
145
|
-
this.send({
|
|
146
|
-
type: "error",
|
|
147
|
-
senderId: this.peerId!,
|
|
148
|
-
message: "unsupported protocol version",
|
|
149
|
-
targetId: senderId,
|
|
150
|
-
})
|
|
151
|
-
this.sockets[senderId].close()
|
|
152
|
-
delete this.sockets[senderId]
|
|
153
|
-
} else {
|
|
154
|
-
this.send({
|
|
155
|
-
type: "peer",
|
|
156
|
-
senderId: this.peerId!,
|
|
157
|
-
selectedProtocolVersion: ProtocolV1,
|
|
158
|
-
targetId: senderId,
|
|
143
|
+
const { storageId, isEphemeral } = cbor
|
|
144
|
+
// Let the rest of the system know that we have a new connection.
|
|
145
|
+
this.emit("peer-candidate", {
|
|
146
|
+
peerId: senderId,
|
|
147
|
+
storageId,
|
|
148
|
+
isEphemeral,
|
|
159
149
|
})
|
|
150
|
+
this.sockets[senderId] = socket
|
|
151
|
+
|
|
152
|
+
// In this client-server connection, there's only ever one peer: us!
|
|
153
|
+
// (and we pretend to be joined to every channel)
|
|
154
|
+
const selectedProtocolVersion = selectProtocol(
|
|
155
|
+
cbor.supportedProtocolVersions
|
|
156
|
+
)
|
|
157
|
+
if (selectedProtocolVersion === null) {
|
|
158
|
+
this.send({
|
|
159
|
+
type: "error",
|
|
160
|
+
senderId: this.peerId!,
|
|
161
|
+
message: "unsupported protocol version",
|
|
162
|
+
targetId: senderId,
|
|
163
|
+
})
|
|
164
|
+
this.sockets[senderId].close()
|
|
165
|
+
delete this.sockets[senderId]
|
|
166
|
+
} else {
|
|
167
|
+
this.send({
|
|
168
|
+
type: "peer",
|
|
169
|
+
senderId: this.peerId!,
|
|
170
|
+
storageId: this.storageId,
|
|
171
|
+
isEphemeral: this.isEphemeral,
|
|
172
|
+
selectedProtocolVersion: ProtocolV1,
|
|
173
|
+
targetId: senderId,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
160
176
|
}
|
|
161
177
|
break
|
|
162
178
|
case "leave":
|
package/src/messages.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Message, PeerId } from "@automerge/automerge-repo"
|
|
1
|
+
import type { Message, PeerId, StorageId } from "@automerge/automerge-repo"
|
|
2
2
|
import type { ProtocolVersion } from "./protocolVersion.js"
|
|
3
3
|
|
|
4
4
|
/** The sender is disconnecting */
|
|
@@ -12,6 +12,14 @@ export type JoinMessage = {
|
|
|
12
12
|
type: "join"
|
|
13
13
|
/** The PeerID of the client */
|
|
14
14
|
senderId: PeerId
|
|
15
|
+
|
|
16
|
+
/** Unique ID of the storage that the sender peer is using, is persistent across sessions */
|
|
17
|
+
storageId?: StorageId
|
|
18
|
+
|
|
19
|
+
/** Indicates whether other peers should persist the sync state of the sender peer.
|
|
20
|
+
* Sync state is only persisted for non-ephemeral peers */
|
|
21
|
+
isEphemeral: boolean
|
|
22
|
+
|
|
15
23
|
/** The protocol version the client supports */
|
|
16
24
|
supportedProtocolVersions: ProtocolVersion[]
|
|
17
25
|
}
|
|
@@ -21,6 +29,14 @@ export type PeerMessage = {
|
|
|
21
29
|
type: "peer"
|
|
22
30
|
/** The PeerID of the server */
|
|
23
31
|
senderId: PeerId
|
|
32
|
+
|
|
33
|
+
/** Unique ID of the storage that the sender peer is using, is persistent across sessions */
|
|
34
|
+
storageId?: StorageId
|
|
35
|
+
|
|
36
|
+
/** Indicates whether other peers should persist the sync state of the sender peer.
|
|
37
|
+
* Sync state is only persisted for non-ephemeral peers */
|
|
38
|
+
isEphemeral: boolean
|
|
39
|
+
|
|
24
40
|
/** The protocol version the server selected for this connection */
|
|
25
41
|
selectedProtocolVersion: ProtocolVersion
|
|
26
42
|
/** The PeerID of the client */
|
package/test/Websocket.test.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { next as A } from "@automerge/automerge"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AutomergeUrl,
|
|
4
|
+
DocumentId,
|
|
5
|
+
PeerId,
|
|
6
|
+
Repo,
|
|
7
|
+
SyncMessage,
|
|
8
|
+
parseAutomergeUrl,
|
|
9
|
+
} from "@automerge/automerge-repo"
|
|
3
10
|
import assert from "assert"
|
|
4
11
|
import * as CBOR from "cbor-x"
|
|
5
12
|
import { once } from "events"
|
|
@@ -10,7 +17,7 @@ import { runAdapterTests } from "../../automerge-repo/src/helpers/tests/network-
|
|
|
10
17
|
import { DummyStorageAdapter } from "../../automerge-repo/test/helpers/DummyStorageAdapter.js"
|
|
11
18
|
import { BrowserWebSocketClientAdapter } from "../src/BrowserWebSocketClientAdapter.js"
|
|
12
19
|
import { NodeWSServerAdapter } from "../src/NodeWSServerAdapter.js"
|
|
13
|
-
import {headsAreSame} from "@automerge/automerge-repo/src/helpers/headsAreSame.js"
|
|
20
|
+
import { headsAreSame } from "@automerge/automerge-repo/src/helpers/headsAreSame.js"
|
|
14
21
|
|
|
15
22
|
describe("Websocket adapters", () => {
|
|
16
23
|
const setup = async (clientCount = 1) => {
|
|
@@ -73,6 +80,8 @@ describe("Websocket adapters", () => {
|
|
|
73
80
|
assert.deepEqual(message, {
|
|
74
81
|
type: "join",
|
|
75
82
|
senderId: "browser",
|
|
83
|
+
storageId: undefined,
|
|
84
|
+
isEphemeral: true,
|
|
76
85
|
supportedProtocolVersions: ["1"],
|
|
77
86
|
})
|
|
78
87
|
})
|
|
@@ -122,12 +131,12 @@ describe("Websocket adapters", () => {
|
|
|
122
131
|
} = await setup()
|
|
123
132
|
|
|
124
133
|
const peerId = "testclient" as PeerId
|
|
125
|
-
browser.connect(peerId)
|
|
134
|
+
browser.connect(peerId, undefined, true)
|
|
126
135
|
|
|
127
136
|
// simulate the reconnect timer firing before the other end has responded
|
|
128
137
|
// (which works here because we haven't yielded to the event loop yet so
|
|
129
138
|
// the server, which is on the same event loop as us, can't respond)
|
|
130
|
-
browser.connect(peerId)
|
|
139
|
+
browser.connect(peerId, undefined, true)
|
|
131
140
|
|
|
132
141
|
// Now yield, so the server responds on the first socket, if the listeners
|
|
133
142
|
// are cleaned up correctly we shouldn't throw
|
|
@@ -156,11 +165,11 @@ describe("Websocket adapters", () => {
|
|
|
156
165
|
}
|
|
157
166
|
|
|
158
167
|
async function recvOrTimeout(socket: WebSocket): Promise<Buffer | null> {
|
|
159
|
-
return new Promise(
|
|
168
|
+
return new Promise(resolve => {
|
|
160
169
|
const timer = setTimeout(() => {
|
|
161
170
|
resolve(null)
|
|
162
171
|
}, 1000)
|
|
163
|
-
socket.once("message",
|
|
172
|
+
socket.once("message", msg => {
|
|
164
173
|
clearTimeout(timer)
|
|
165
174
|
resolve(msg as Buffer)
|
|
166
175
|
})
|
|
@@ -176,6 +185,8 @@ describe("Websocket adapters", () => {
|
|
|
176
185
|
assert.deepEqual(response, {
|
|
177
186
|
type: "peer",
|
|
178
187
|
senderId: "server",
|
|
188
|
+
storageId: undefined,
|
|
189
|
+
isEphemeral: true,
|
|
179
190
|
targetId: "browser",
|
|
180
191
|
selectedProtocolVersion: "1",
|
|
181
192
|
})
|
|
@@ -203,26 +214,30 @@ describe("Websocket adapters", () => {
|
|
|
203
214
|
assert.deepEqual(response, {
|
|
204
215
|
type: "peer",
|
|
205
216
|
senderId: "server",
|
|
217
|
+
storageId: undefined,
|
|
218
|
+
isEphemeral: true,
|
|
206
219
|
targetId: "browser",
|
|
207
220
|
selectedProtocolVersion: "1",
|
|
208
221
|
})
|
|
209
222
|
})
|
|
210
223
|
|
|
211
|
-
/**
|
|
224
|
+
/**
|
|
212
225
|
* Create a new document, initialized with the given contents and return a
|
|
213
226
|
* storage containign that document as well as the URL and a fork of the
|
|
214
227
|
* document
|
|
215
228
|
*
|
|
216
229
|
* @param contents - The contents to initialize the document with
|
|
217
230
|
*/
|
|
218
|
-
async function initDocAndStorage<T extends Record<string, unknown>>(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
231
|
+
async function initDocAndStorage<T extends Record<string, unknown>>(
|
|
232
|
+
contents: T
|
|
233
|
+
): Promise<{
|
|
234
|
+
storage: DummyStorageAdapter
|
|
235
|
+
url: AutomergeUrl
|
|
236
|
+
doc: A.Doc<T>
|
|
237
|
+
documentId: DocumentId
|
|
238
|
+
}> {
|
|
224
239
|
const storage = new DummyStorageAdapter()
|
|
225
|
-
const silentRepo = new Repo({storage, network: []})
|
|
240
|
+
const silentRepo = new Repo({ storage, network: [] })
|
|
226
241
|
const doc = A.from<T>(contents)
|
|
227
242
|
const handle = silentRepo.create()
|
|
228
243
|
handle.update(() => A.clone(doc))
|
|
@@ -246,7 +261,10 @@ describe("Websocket adapters", () => {
|
|
|
246
261
|
}
|
|
247
262
|
}
|
|
248
263
|
|
|
249
|
-
function assertIsSyncMessage(
|
|
264
|
+
function assertIsSyncMessage(
|
|
265
|
+
forDocument: DocumentId,
|
|
266
|
+
msg: Buffer | null
|
|
267
|
+
): SyncMessage {
|
|
250
268
|
if (msg == null) {
|
|
251
269
|
throw new Error("expected a peer message, got null")
|
|
252
270
|
}
|
|
@@ -255,13 +273,15 @@ describe("Websocket adapters", () => {
|
|
|
255
273
|
throw new Error(`expected a peer message, got type: ${decoded.type}`)
|
|
256
274
|
}
|
|
257
275
|
if (decoded.documentId !== forDocument) {
|
|
258
|
-
throw new Error(
|
|
276
|
+
throw new Error(
|
|
277
|
+
`expected a sync message for ${forDocument}, not for ${decoded.documentId}`
|
|
278
|
+
)
|
|
259
279
|
}
|
|
260
280
|
return decoded
|
|
261
281
|
}
|
|
262
282
|
|
|
263
283
|
it("should disconnect existing peers on reconnect before announcing them", async () => {
|
|
264
|
-
// This test exercises a sync loop which is exposed in the following
|
|
284
|
+
// This test exercises a sync loop which is exposed in the following
|
|
265
285
|
// sequence of events:
|
|
266
286
|
//
|
|
267
287
|
// 1. A document exists on both the server and the client with divergent
|
|
@@ -276,27 +296,33 @@ describe("Websocket adapters", () => {
|
|
|
276
296
|
// asks for them
|
|
277
297
|
// 7. The server responds with an empty sync message because it thinks it
|
|
278
298
|
// has already sent the changes
|
|
279
|
-
//
|
|
299
|
+
//
|
|
280
300
|
// 6 and 7 continue in an infinite loop. The root cause is the servers
|
|
281
301
|
// failure to clear the sync state associated with the given peer when
|
|
282
302
|
// it receives a new connection from the same peer ID.
|
|
283
303
|
const { socket, serverUrl } = await setup(0)
|
|
284
304
|
|
|
285
305
|
// Create a doc, populate a DummyStorageAdapter with that doc
|
|
286
|
-
const {storage, url, doc, documentId} = await initDocAndStorage({
|
|
306
|
+
const { storage, url, doc, documentId } = await initDocAndStorage({
|
|
307
|
+
foo: "bar",
|
|
308
|
+
})
|
|
287
309
|
|
|
288
310
|
// Create a copy of the document to represent the client state
|
|
289
|
-
let clientDoc = A.clone<{foo: string}>(doc)
|
|
290
|
-
clientDoc = A.change(clientDoc, d => d.foo = "qux")
|
|
311
|
+
let clientDoc = A.clone<{ foo: string }>(doc)
|
|
312
|
+
clientDoc = A.change(clientDoc, d => (d.foo = "qux"))
|
|
291
313
|
|
|
292
314
|
// Now create a websocket sync server with the original document in it's storage
|
|
293
315
|
const adapter = new NodeWSServerAdapter(socket)
|
|
294
|
-
const repo = new Repo({
|
|
316
|
+
const repo = new Repo({
|
|
317
|
+
network: [adapter],
|
|
318
|
+
storage,
|
|
319
|
+
peerId: "server" as PeerId,
|
|
320
|
+
})
|
|
295
321
|
|
|
296
322
|
// make a change to the handle on the sync server
|
|
297
|
-
const handle = repo.find<{foo: string}>(url)
|
|
323
|
+
const handle = repo.find<{ foo: string }>(url)
|
|
298
324
|
await handle.whenReady()
|
|
299
|
-
handle.change(d => d.foo = "baz")
|
|
325
|
+
handle.change(d => (d.foo = "baz"))
|
|
300
326
|
|
|
301
327
|
// Okay, so now there is a document on both the client and the server
|
|
302
328
|
// which has concurrent changes on each peer.
|
|
@@ -306,11 +332,13 @@ describe("Websocket adapters", () => {
|
|
|
306
332
|
await once(clientSocket, "open")
|
|
307
333
|
|
|
308
334
|
// Run through the client/server hello
|
|
309
|
-
clientSocket.send(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
335
|
+
clientSocket.send(
|
|
336
|
+
CBOR.encode({
|
|
337
|
+
type: "join",
|
|
338
|
+
senderId: "client",
|
|
339
|
+
supportedProtocolVersions: ["1"],
|
|
340
|
+
})
|
|
341
|
+
)
|
|
314
342
|
|
|
315
343
|
let response = await recvOrTimeout(clientSocket)
|
|
316
344
|
assertIsPeerMessage(response)
|
|
@@ -318,24 +346,29 @@ describe("Websocket adapters", () => {
|
|
|
318
346
|
// Okay now we start syncing
|
|
319
347
|
|
|
320
348
|
let clientState = A.initSyncState()
|
|
321
|
-
let [newSyncState, message] = A.generateSyncMessage(
|
|
349
|
+
let [newSyncState, message] = A.generateSyncMessage(
|
|
350
|
+
clientDoc,
|
|
351
|
+
clientState
|
|
352
|
+
)
|
|
322
353
|
clientState = newSyncState
|
|
323
354
|
|
|
324
355
|
// Send the initial sync state
|
|
325
|
-
clientSocket.send(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
356
|
+
clientSocket.send(
|
|
357
|
+
CBOR.encode({
|
|
358
|
+
type: "request",
|
|
359
|
+
documentId,
|
|
360
|
+
targetId: "server",
|
|
361
|
+
senderId: "client",
|
|
362
|
+
data: message,
|
|
363
|
+
})
|
|
364
|
+
)
|
|
332
365
|
|
|
333
366
|
response = await recvOrTimeout(clientSocket)
|
|
334
367
|
assertIsSyncMessage(documentId, response)
|
|
335
368
|
|
|
336
|
-
// Now, assume either the network or the server is going slow, so the
|
|
369
|
+
// Now, assume either the network or the server is going slow, so the
|
|
337
370
|
// server thinks it has sent the response above, but for whatever reason
|
|
338
|
-
// it never gets to the client. In that case the reconnect timer in the
|
|
371
|
+
// it never gets to the client. In that case the reconnect timer in the
|
|
339
372
|
// BrowserWebSocketClientAdapter will fire and we'll create a new
|
|
340
373
|
// websocket and connect it. To simulate this we drop the above response
|
|
341
374
|
// on the floor and start connecting again.
|
|
@@ -344,34 +377,42 @@ describe("Websocket adapters", () => {
|
|
|
344
377
|
await once(clientSocket, "open")
|
|
345
378
|
|
|
346
379
|
// and we also make a change to the client doc
|
|
347
|
-
clientDoc = A.change(clientDoc, d => d.foo = "quoxen")
|
|
380
|
+
clientDoc = A.change(clientDoc, d => (d.foo = "quoxen"))
|
|
348
381
|
|
|
349
382
|
// Run through the whole client/server hello dance again
|
|
350
|
-
clientSocket.send(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
383
|
+
clientSocket.send(
|
|
384
|
+
CBOR.encode({
|
|
385
|
+
type: "join",
|
|
386
|
+
senderId: "client",
|
|
387
|
+
supportedProtocolVersions: ["1"],
|
|
388
|
+
})
|
|
389
|
+
)
|
|
355
390
|
|
|
356
391
|
response = await recvOrTimeout(clientSocket)
|
|
357
392
|
assertIsPeerMessage(response)
|
|
358
393
|
|
|
359
394
|
// Now, we start syncing. If we're not buggy, this loop should terminate.
|
|
360
|
-
while(true) {
|
|
395
|
+
while (true) {
|
|
361
396
|
;[clientState, message] = A.generateSyncMessage(clientDoc, clientState)
|
|
362
397
|
if (message) {
|
|
363
|
-
clientSocket.send(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
398
|
+
clientSocket.send(
|
|
399
|
+
CBOR.encode({
|
|
400
|
+
type: "sync",
|
|
401
|
+
documentId,
|
|
402
|
+
targetId: "server",
|
|
403
|
+
senderId: "client",
|
|
404
|
+
data: message,
|
|
405
|
+
})
|
|
406
|
+
)
|
|
370
407
|
}
|
|
371
408
|
const response = await recvOrTimeout(clientSocket)
|
|
372
409
|
if (response) {
|
|
373
410
|
const decoded = assertIsSyncMessage(documentId, response)
|
|
374
|
-
;[clientDoc, clientState] = A.receiveSyncMessage(
|
|
411
|
+
;[clientDoc, clientState] = A.receiveSyncMessage(
|
|
412
|
+
clientDoc,
|
|
413
|
+
clientState,
|
|
414
|
+
decoded.data
|
|
415
|
+
)
|
|
375
416
|
}
|
|
376
417
|
if (response == null && message == null) {
|
|
377
418
|
break
|