@automerge/automerge-repo-network-websocket 1.0.18 → 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.
@@ -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,EAAE,cAAc,EAAE,MAAM,EAAQ,MAAM,2BAA2B,CAAA;AACxE,OAAO,SAAS,MAAM,eAAe,CAAA;AAIrC,OAAO,EACL,iBAAiB,EAGlB,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,CAAC,MAAM,EAAE,MAAM;IAkCtB,MAAM,aAKL;IAGD,OAAO,aAYN;IAED,SAAS,UAAW,sBAAsB,UAEzC;IAED,IAAI;IAWJ,UAAU;IAOV,IAAI,CAAC,OAAO,EAAE,iBAAiB;IAwB/B,kBAAkB,CAAC,MAAM,EAAE,MAAM;IAcjC,cAAc,CAAC,OAAO,EAAE,UAAU;CA0BnC"}
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,EACZ,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,CAAC,MAAM,EAAE,MAAM;IAoDtB,UAAU;IAIV,IAAI,CAAC,OAAO,EAAE,iBAAiB;IAyB/B,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS;CA+DtD"}
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
- const existingSocket = this.sockets[senderId];
88
- if (existingSocket) {
89
- if (existingSocket.readyState === WebSocket.OPEN) {
90
- existingSocket.close();
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
- this.emit("peer-disconnected", { peerId: senderId });
93
- }
94
- // Let the rest of the system know that we have a new connection.
95
- this.emit("peer-candidate", { peerId: senderId });
96
- this.sockets[senderId] = socket;
97
- // In this client-server connection, there's only ever one peer: us!
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":
@@ -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 */
@@ -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;AAChE,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;IAChB,+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;IAChB,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"}
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.18",
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.18",
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": "737bde99e0387bef70b53cb0b14d8f08f48f7a92"
33
+ "gitHead": "11805d698f860bd6ffb3ca028d3b57e718690b5a"
34
34
  }
@@ -1,4 +1,9 @@
1
- import { NetworkAdapter, PeerId, cbor } from "@automerge/automerge-repo"
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(peerId: PeerId) {
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(() => this.connect(peerId), 5000)
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(peerId: PeerId) {
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(senderId: PeerId): 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(peerId: PeerId) {
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
- const existingSocket = this.sockets[senderId]
128
- if (existingSocket) {
129
- if (existingSocket.readyState === WebSocket.OPEN) {
130
- existingSocket.close()
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
- // Let the rest of the system know that we have a new connection.
136
- this.emit("peer-candidate", { peerId: senderId })
137
- this.sockets[senderId] = socket
138
-
139
- // In this client-server connection, there's only ever one peer: us!
140
- // (and we pretend to be joined to every channel)
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 */
@@ -1,5 +1,12 @@
1
1
  import { next as A } from "@automerge/automerge"
2
- import { AutomergeUrl, DocumentId, PeerId, Repo, SyncMessage, parseAutomergeUrl } from "@automerge/automerge-repo"
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((resolve) => {
168
+ return new Promise(resolve => {
160
169
  const timer = setTimeout(() => {
161
170
  resolve(null)
162
171
  }, 1000)
163
- socket.once("message", (msg) => {
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>>(contents: T): Promise<{
219
- storage: DummyStorageAdapter,
220
- url: AutomergeUrl,
221
- doc: A.Doc<T>,
222
- documentId: DocumentId
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(forDocument: DocumentId, msg: Buffer | null): SyncMessage {
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(`expected a sync message for ${forDocument}, not for ${decoded.documentId}`)
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({foo: "bar"})
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({ network: [adapter], storage, peerId: "server" as PeerId })
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(CBOR.encode({
310
- type: "join",
311
- senderId: "client",
312
- supportedProtocolVersions: ["1"],
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(clientDoc, clientState)
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(CBOR.encode({
326
- type: "request",
327
- documentId,
328
- targetId: "server",
329
- senderId: "client",
330
- data: message
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(CBOR.encode({
351
- type: "join",
352
- senderId: "client",
353
- supportedProtocolVersions: ["1"],
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(CBOR.encode({
364
- type: "sync",
365
- documentId,
366
- targetId: "server",
367
- senderId: "client",
368
- data: message
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(clientDoc, clientState, decoded.data)
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