@automerge/automerge-repo 1.0.0-alpha.2 → 1.0.0-alpha.3
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/DocCollection.d.ts +2 -1
- package/dist/DocCollection.d.ts.map +1 -1
- package/dist/DocCollection.js +17 -8
- package/dist/DocHandle.d.ts +27 -4
- package/dist/DocHandle.d.ts.map +1 -1
- package/dist/DocHandle.js +44 -6
- package/dist/DocUrl.d.ts +3 -3
- package/dist/DocUrl.js +9 -9
- package/dist/EphemeralData.d.ts +8 -16
- package/dist/EphemeralData.d.ts.map +1 -1
- package/dist/EphemeralData.js +1 -28
- package/dist/Repo.d.ts +0 -2
- package/dist/Repo.d.ts.map +1 -1
- package/dist/Repo.js +13 -33
- package/dist/helpers/tests/network-adapter-tests.d.ts.map +1 -1
- package/dist/helpers/tests/network-adapter-tests.js +15 -13
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/network/NetworkAdapter.d.ts +4 -13
- package/dist/network/NetworkAdapter.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.d.ts +5 -4
- package/dist/network/NetworkSubsystem.d.ts.map +1 -1
- package/dist/network/NetworkSubsystem.js +39 -25
- package/dist/network/messages.d.ts +57 -0
- package/dist/network/messages.d.ts.map +1 -0
- package/dist/network/messages.js +21 -0
- package/dist/synchronizer/CollectionSynchronizer.d.ts +3 -2
- package/dist/synchronizer/CollectionSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/CollectionSynchronizer.js +19 -13
- package/dist/synchronizer/DocSynchronizer.d.ts +9 -3
- package/dist/synchronizer/DocSynchronizer.d.ts.map +1 -1
- package/dist/synchronizer/DocSynchronizer.js +145 -29
- package/dist/synchronizer/Synchronizer.d.ts +3 -4
- package/dist/synchronizer/Synchronizer.d.ts.map +1 -1
- package/dist/types.d.ts +1 -3
- package/dist/types.d.ts.map +1 -1
- package/fuzz/fuzz.ts +4 -4
- package/package.json +2 -2
- package/src/DocCollection.ts +19 -9
- package/src/DocHandle.ts +87 -10
- package/src/DocUrl.ts +9 -9
- package/src/EphemeralData.ts +6 -36
- package/src/Repo.ts +15 -49
- package/src/helpers/tests/network-adapter-tests.ts +18 -14
- package/src/index.ts +12 -2
- package/src/network/NetworkAdapter.ts +4 -20
- package/src/network/NetworkSubsystem.ts +61 -38
- package/src/network/messages.ts +123 -0
- package/src/synchronizer/CollectionSynchronizer.ts +38 -19
- package/src/synchronizer/DocSynchronizer.ts +196 -38
- package/src/synchronizer/Synchronizer.ts +3 -8
- package/src/types.ts +4 -1
- package/test/CollectionSynchronizer.test.ts +6 -7
- package/test/DocHandle.test.ts +28 -13
- package/test/DocSynchronizer.test.ts +85 -9
- package/test/Repo.test.ts +221 -59
- package/test/StorageSubsystem.test.ts +2 -2
- package/test/helpers/DummyNetworkAdapter.ts +1 -1
- package/tsconfig.json +2 -1
- package/test/EphemeralData.test.ts +0 -44
package/src/EphemeralData.ts
CHANGED
|
@@ -1,46 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { ChannelId, PeerId } from "./index.js"
|
|
4
|
-
import { MessagePayload } from "./network/NetworkAdapter.js"
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* EphemeralData provides a mechanism to broadcast short-lived data — cursor positions, presence,
|
|
8
|
-
* heartbeats, etc. — that is useful in the moment but not worth persisting.
|
|
9
|
-
*/
|
|
10
|
-
export class EphemeralData extends EventEmitter<EphemeralDataMessageEvents> {
|
|
11
|
-
/** Broadcast an ephemeral message */
|
|
12
|
-
broadcast(channelId: ChannelId, message: unknown) {
|
|
13
|
-
const messageBytes = encode(message)
|
|
14
|
-
|
|
15
|
-
this.emit("message", {
|
|
16
|
-
targetId: "*" as PeerId, // TODO: we don't really need a targetId for broadcast
|
|
17
|
-
channelId: ("m/" + channelId) as ChannelId,
|
|
18
|
-
message: messageBytes,
|
|
19
|
-
broadcast: true,
|
|
20
|
-
})
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Receive an ephemeral message */
|
|
24
|
-
receive(senderId: PeerId, grossChannelId: ChannelId, message: Uint8Array) {
|
|
25
|
-
const data = decode(message)
|
|
26
|
-
const channelId = grossChannelId.slice(2) as ChannelId
|
|
27
|
-
this.emit("data", {
|
|
28
|
-
peerId: senderId,
|
|
29
|
-
channelId,
|
|
30
|
-
data,
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
import { DocumentId, PeerId } from "./index.js"
|
|
2
|
+
import { EphemeralMessageContents } from "./network/messages.js"
|
|
34
3
|
|
|
35
4
|
// types
|
|
5
|
+
export type SessionId = string & { __SessionId: false }
|
|
36
6
|
|
|
37
7
|
export interface EphemeralDataPayload {
|
|
38
|
-
|
|
8
|
+
documentId: DocumentId
|
|
39
9
|
peerId: PeerId
|
|
40
|
-
data: { peerId: PeerId;
|
|
10
|
+
data: { peerId: PeerId; documentId: DocumentId; data: unknown }
|
|
41
11
|
}
|
|
42
12
|
|
|
43
13
|
export type EphemeralDataMessageEvents = {
|
|
44
|
-
message: (event:
|
|
14
|
+
message: (event: EphemeralMessageContents) => void
|
|
45
15
|
data: (event: EphemeralDataPayload) => void
|
|
46
16
|
}
|
package/src/Repo.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import debug from "debug"
|
|
1
2
|
import { DocCollection } from "./DocCollection.js"
|
|
2
|
-
import { EphemeralData } from "./EphemeralData.js"
|
|
3
3
|
import { NetworkAdapter } from "./network/NetworkAdapter.js"
|
|
4
4
|
import { NetworkSubsystem } from "./network/NetworkSubsystem.js"
|
|
5
5
|
import { StorageAdapter } from "./storage/StorageAdapter.js"
|
|
@@ -7,15 +7,12 @@ import { StorageSubsystem } from "./storage/StorageSubsystem.js"
|
|
|
7
7
|
import { CollectionSynchronizer } from "./synchronizer/CollectionSynchronizer.js"
|
|
8
8
|
import { DocumentId, PeerId } from "./types.js"
|
|
9
9
|
|
|
10
|
-
import debug from "debug"
|
|
11
|
-
|
|
12
10
|
/** A Repo is a DocCollection with networking, syncing, and storage capabilities. */
|
|
13
11
|
export class Repo extends DocCollection {
|
|
14
12
|
#log: debug.Debugger
|
|
15
13
|
|
|
16
14
|
networkSubsystem: NetworkSubsystem
|
|
17
15
|
storageSubsystem?: StorageSubsystem
|
|
18
|
-
ephemeralData: EphemeralData
|
|
19
16
|
|
|
20
17
|
constructor({ storage, network, peerId, sharePolicy }: RepoConfig) {
|
|
21
18
|
super()
|
|
@@ -40,44 +37,45 @@ export class Repo extends DocCollection {
|
|
|
40
37
|
}
|
|
41
38
|
}
|
|
42
39
|
|
|
40
|
+
handle.on("unavailable", () => {
|
|
41
|
+
this.#log("document unavailable", { documentId: handle.documentId })
|
|
42
|
+
this.emit("unavailable-document", {
|
|
43
|
+
documentId: handle.documentId,
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
43
47
|
handle.request()
|
|
44
48
|
|
|
45
49
|
// Register the document with the synchronizer. This advertises our interest in the document.
|
|
46
50
|
synchronizer.addDocument(handle.documentId)
|
|
47
51
|
})
|
|
48
52
|
|
|
49
|
-
this.on("delete-document", ({
|
|
53
|
+
this.on("delete-document", ({ documentId }) => {
|
|
50
54
|
// TODO Pass the delete on to the network
|
|
51
55
|
// synchronizer.removeDocument(documentId)
|
|
52
56
|
|
|
53
57
|
if (storageSubsystem) {
|
|
54
|
-
storageSubsystem.remove(
|
|
58
|
+
storageSubsystem.remove(documentId)
|
|
55
59
|
}
|
|
56
60
|
})
|
|
57
61
|
|
|
58
62
|
// SYNCHRONIZER
|
|
59
63
|
// The synchronizer uses the network subsystem to keep documents in sync with peers.
|
|
60
|
-
|
|
61
64
|
const synchronizer = new CollectionSynchronizer(this)
|
|
62
65
|
|
|
63
66
|
// When the synchronizer emits sync messages, send them to peers
|
|
64
|
-
synchronizer.on(
|
|
65
|
-
|
|
66
|
-
(
|
|
67
|
-
|
|
68
|
-
networkSubsystem.sendMessage(targetId, channelId, message, broadcast)
|
|
69
|
-
}
|
|
70
|
-
)
|
|
67
|
+
synchronizer.on("message", message => {
|
|
68
|
+
this.#log(`sending sync message to ${message.targetId}`)
|
|
69
|
+
networkSubsystem.send(message)
|
|
70
|
+
})
|
|
71
71
|
|
|
72
72
|
// STORAGE
|
|
73
73
|
// The storage subsystem has access to some form of persistence, and deals with save and loading documents.
|
|
74
|
-
|
|
75
74
|
const storageSubsystem = storage ? new StorageSubsystem(storage) : undefined
|
|
76
75
|
this.storageSubsystem = storageSubsystem
|
|
77
76
|
|
|
78
77
|
// NETWORK
|
|
79
78
|
// The network subsystem deals with sending and receiving messages to and from peers.
|
|
80
|
-
|
|
81
79
|
const networkSubsystem = new NetworkSubsystem(network, peerId)
|
|
82
80
|
this.networkSubsystem = networkSubsystem
|
|
83
81
|
|
|
@@ -94,40 +92,8 @@ export class Repo extends DocCollection {
|
|
|
94
92
|
|
|
95
93
|
// Handle incoming messages
|
|
96
94
|
networkSubsystem.on("message", async msg => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// TODO: this demands a more principled way of associating channels with recipients
|
|
100
|
-
|
|
101
|
-
// Ephemeral channel ids start with "m/"
|
|
102
|
-
if (channelId.startsWith("m/")) {
|
|
103
|
-
// Ephemeral message
|
|
104
|
-
this.#log(`receiving ephemeral message from ${senderId}`)
|
|
105
|
-
ephemeralData.receive(senderId, channelId, message)
|
|
106
|
-
} else {
|
|
107
|
-
// Sync message
|
|
108
|
-
this.#log(`receiving sync message from ${senderId}`)
|
|
109
|
-
await synchronizer.receiveSyncMessage(senderId, channelId, message)
|
|
110
|
-
}
|
|
95
|
+
await synchronizer.receiveMessage(msg)
|
|
111
96
|
})
|
|
112
|
-
|
|
113
|
-
// We establish a special channel for sync messages
|
|
114
|
-
networkSubsystem.join()
|
|
115
|
-
|
|
116
|
-
// EPHEMERAL DATA
|
|
117
|
-
// The ephemeral data subsystem uses the network to send and receive messages that are not
|
|
118
|
-
// persisted to storage, e.g. cursor position, presence, etc.
|
|
119
|
-
|
|
120
|
-
const ephemeralData = new EphemeralData()
|
|
121
|
-
this.ephemeralData = ephemeralData
|
|
122
|
-
|
|
123
|
-
// Send ephemeral messages to peers
|
|
124
|
-
ephemeralData.on(
|
|
125
|
-
"message",
|
|
126
|
-
({ targetId, channelId, message, broadcast }) => {
|
|
127
|
-
this.#log(`sending ephemeral message to ${targetId}`)
|
|
128
|
-
networkSubsystem.sendMessage(targetId, channelId, message, broadcast)
|
|
129
|
-
}
|
|
130
|
-
)
|
|
131
97
|
}
|
|
132
98
|
}
|
|
133
99
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { PeerId, Repo, type NetworkAdapter,
|
|
1
|
+
import { PeerId, Repo, type NetworkAdapter, DocumentId } from "../../index.js"
|
|
2
2
|
import { eventPromise, eventPromises } from "../eventPromise.js"
|
|
3
3
|
import { assert } from "chai"
|
|
4
4
|
import { describe, it } from "mocha"
|
|
5
|
+
import { pause } from "../pause.js"
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Runs a series of tests against a set of three peers, each represented by one or more instantiated
|
|
@@ -46,7 +47,7 @@ export function runAdapterTests(_setup: SetupFn, title?: string): void {
|
|
|
46
47
|
|
|
47
48
|
// Bob receives the change
|
|
48
49
|
await eventPromise(bobHandle, "change")
|
|
49
|
-
assert.equal((await bobHandle.doc())
|
|
50
|
+
assert.equal((await bobHandle.doc())?.foo, "bar")
|
|
50
51
|
|
|
51
52
|
// Bob changes the document
|
|
52
53
|
bobHandle.change(d => {
|
|
@@ -55,7 +56,7 @@ export function runAdapterTests(_setup: SetupFn, title?: string): void {
|
|
|
55
56
|
|
|
56
57
|
// Alice receives the change
|
|
57
58
|
await eventPromise(aliceHandle, "change")
|
|
58
|
-
assert.equal((await aliceHandle.doc())
|
|
59
|
+
assert.equal((await aliceHandle.doc())?.foo, "baz")
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
// Run the test in both directions, in case they're different types of adapters
|
|
@@ -97,8 +98,8 @@ export function runAdapterTests(_setup: SetupFn, title?: string): void {
|
|
|
97
98
|
|
|
98
99
|
// Bob and Charlie receive the change
|
|
99
100
|
await eventPromises([bobHandle, charlieHandle], "change")
|
|
100
|
-
assert.equal((await bobHandle.doc())
|
|
101
|
-
assert.equal((await charlieHandle.doc())
|
|
101
|
+
assert.equal((await bobHandle.doc())?.foo, "bar")
|
|
102
|
+
assert.equal((await charlieHandle.doc())?.foo, "bar")
|
|
102
103
|
|
|
103
104
|
// Charlie changes the document
|
|
104
105
|
charlieHandle.change(d => {
|
|
@@ -107,15 +108,13 @@ export function runAdapterTests(_setup: SetupFn, title?: string): void {
|
|
|
107
108
|
|
|
108
109
|
// Alice and Bob receive the change
|
|
109
110
|
await eventPromises([aliceHandle, bobHandle], "change")
|
|
110
|
-
assert.equal((await bobHandle.doc())
|
|
111
|
-
assert.equal((await charlieHandle.doc())
|
|
111
|
+
assert.equal((await bobHandle.doc())?.foo, "baz")
|
|
112
|
+
assert.equal((await charlieHandle.doc())?.foo, "baz")
|
|
112
113
|
|
|
113
114
|
teardown()
|
|
114
115
|
})
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
// because the network has cycles (see #92)
|
|
118
|
-
it.skip("can broadcast a message", async () => {
|
|
117
|
+
it("can broadcast a message", async () => {
|
|
119
118
|
const { adapters, teardown } = await setup()
|
|
120
119
|
const [a, b, c] = adapters
|
|
121
120
|
|
|
@@ -128,13 +127,18 @@ export function runAdapterTests(_setup: SetupFn, title?: string): void {
|
|
|
128
127
|
"peer"
|
|
129
128
|
)
|
|
130
129
|
|
|
131
|
-
const
|
|
130
|
+
const aliceHandle = aliceRepo.create<TestDoc>()
|
|
131
|
+
const charlieHandle = charlieRepo.find(aliceHandle.url)
|
|
132
|
+
|
|
133
|
+
// pause to give charlie a chance to let alice know it wants the doc
|
|
134
|
+
await pause(100)
|
|
135
|
+
|
|
132
136
|
const alicePresenceData = { presence: "alice" }
|
|
137
|
+
aliceHandle.broadcast(alicePresenceData)
|
|
133
138
|
|
|
134
|
-
|
|
135
|
-
const { data } = await eventPromise(charlieRepo.ephemeralData, "data")
|
|
139
|
+
const { message } = await eventPromise(charlieHandle, "ephemeral-message")
|
|
136
140
|
|
|
137
|
-
assert.deepStrictEqual(
|
|
141
|
+
assert.deepStrictEqual(message, alicePresenceData)
|
|
138
142
|
teardown()
|
|
139
143
|
})
|
|
140
144
|
})
|
package/src/index.ts
CHANGED
|
@@ -3,12 +3,22 @@ export { DocHandle, HandleState } from "./DocHandle.js"
|
|
|
3
3
|
export type { DocHandleChangePayload } from "./DocHandle.js"
|
|
4
4
|
export { NetworkAdapter } from "./network/NetworkAdapter.js"
|
|
5
5
|
export type {
|
|
6
|
-
InboundMessagePayload,
|
|
7
|
-
MessagePayload,
|
|
8
6
|
OpenPayload,
|
|
9
7
|
PeerCandidatePayload,
|
|
10
8
|
PeerDisconnectedPayload,
|
|
11
9
|
} from "./network/NetworkAdapter.js"
|
|
10
|
+
|
|
11
|
+
// This is a bit confusing right now, but:
|
|
12
|
+
// Message is the type for messages used outside of the network adapters
|
|
13
|
+
// there are some extra internal network adapter-only messages on NetworkAdapterMessage
|
|
14
|
+
// and Message is (as of this writing) a union type for EphmeralMessage and SyncMessage
|
|
15
|
+
export type {
|
|
16
|
+
Message,
|
|
17
|
+
NetworkAdapterMessage,
|
|
18
|
+
EphemeralMessage,
|
|
19
|
+
SyncMessage,
|
|
20
|
+
} from "./network/messages.js"
|
|
21
|
+
|
|
12
22
|
export { NetworkSubsystem } from "./network/NetworkSubsystem.js"
|
|
13
23
|
export { Repo, type SharePolicy } from "./Repo.js"
|
|
14
24
|
export { StorageAdapter, type StorageKey } from "./storage/StorageAdapter.js"
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import EventEmitter from "eventemitter3"
|
|
2
|
-
import { PeerId
|
|
2
|
+
import { PeerId } from "../types.js"
|
|
3
|
+
import { Message } from "./messages.js"
|
|
3
4
|
|
|
4
5
|
export abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> {
|
|
5
6
|
peerId?: PeerId // hmmm, maybe not
|
|
6
7
|
|
|
7
8
|
abstract connect(url?: string): void
|
|
8
9
|
|
|
9
|
-
abstract
|
|
10
|
-
peerId: PeerId,
|
|
11
|
-
channelId: ChannelId,
|
|
12
|
-
message: Uint8Array,
|
|
13
|
-
broadcast: boolean
|
|
14
|
-
): void
|
|
10
|
+
abstract send(message: Message): void
|
|
15
11
|
|
|
16
12
|
abstract join(): void
|
|
17
13
|
|
|
@@ -25,7 +21,7 @@ export interface NetworkAdapterEvents {
|
|
|
25
21
|
close: () => void
|
|
26
22
|
"peer-candidate": (payload: PeerCandidatePayload) => void
|
|
27
23
|
"peer-disconnected": (payload: PeerDisconnectedPayload) => void
|
|
28
|
-
message: (payload:
|
|
24
|
+
message: (payload: Message) => void
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
export interface OpenPayload {
|
|
@@ -36,18 +32,6 @@ export interface PeerCandidatePayload {
|
|
|
36
32
|
peerId: PeerId
|
|
37
33
|
}
|
|
38
34
|
|
|
39
|
-
export interface MessagePayload {
|
|
40
|
-
targetId: PeerId
|
|
41
|
-
channelId: ChannelId
|
|
42
|
-
message: Uint8Array
|
|
43
|
-
broadcast: boolean
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface InboundMessagePayload extends MessagePayload {
|
|
47
|
-
type?: string
|
|
48
|
-
senderId: PeerId
|
|
49
|
-
}
|
|
50
|
-
|
|
51
35
|
export interface PeerDisconnectedPayload {
|
|
52
36
|
peerId: PeerId
|
|
53
37
|
}
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import EventEmitter from "eventemitter3"
|
|
2
|
+
import { PeerId } from "../types.js"
|
|
3
|
+
import { NetworkAdapter, PeerDisconnectedPayload } from "./NetworkAdapter.js"
|
|
4
|
+
|
|
2
5
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
EphemeralMessage,
|
|
7
|
+
isEphemeralMessage,
|
|
8
|
+
isValidMessage,
|
|
9
|
+
Message,
|
|
10
|
+
MessageContents,
|
|
11
|
+
} from "./messages.js"
|
|
8
12
|
|
|
9
13
|
import debug from "debug"
|
|
14
|
+
import { SessionId } from "../EphemeralData.js"
|
|
15
|
+
|
|
16
|
+
type EphemeralMessageSource = `${PeerId}:${SessionId}`
|
|
17
|
+
|
|
18
|
+
const getEphemeralMessageSource = (message: EphemeralMessage) =>
|
|
19
|
+
`${message.senderId}:${message.sessionId}` as EphemeralMessageSource
|
|
10
20
|
|
|
11
21
|
export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
12
22
|
#log: debug.Debugger
|
|
13
23
|
#adaptersByPeer: Record<PeerId, NetworkAdapter> = {}
|
|
14
24
|
|
|
25
|
+
#count = 0
|
|
26
|
+
#sessionId: SessionId = Math.random().toString(36).slice(2) as SessionId
|
|
27
|
+
#ephemeralSessionCounts: Record<EphemeralMessageSource, number> = {}
|
|
28
|
+
|
|
15
29
|
constructor(
|
|
16
30
|
private adapters: NetworkAdapter[],
|
|
17
31
|
public peerId = randomPeerId()
|
|
@@ -44,20 +58,24 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
44
58
|
})
|
|
45
59
|
|
|
46
60
|
networkAdapter.on("message", msg => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
if (!isValidMessage(msg)) {
|
|
62
|
+
this.#log(`invalid message: ${JSON.stringify(msg)}`)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.#log(`message from ${msg.senderId}`)
|
|
67
|
+
|
|
68
|
+
if (isEphemeralMessage(msg)) {
|
|
69
|
+
const source = getEphemeralMessageSource(msg)
|
|
70
|
+
if (
|
|
71
|
+
this.#ephemeralSessionCounts[source] === undefined ||
|
|
72
|
+
msg.count > this.#ephemeralSessionCounts[source]
|
|
73
|
+
) {
|
|
74
|
+
this.#ephemeralSessionCounts[source] = msg.count
|
|
75
|
+
this.emit("message", msg)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
this.emit("message", msg)
|
|
@@ -75,25 +93,30 @@ export class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> {
|
|
|
75
93
|
networkAdapter.join()
|
|
76
94
|
}
|
|
77
95
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
send(message: MessageContents) {
|
|
97
|
+
const peer = this.#adaptersByPeer[message.targetId]
|
|
98
|
+
if (!peer) {
|
|
99
|
+
this.#log(`Tried to send message but peer not found: ${message.targetId}`)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
this.#log(`Sending message to ${message.targetId}`)
|
|
103
|
+
|
|
104
|
+
if (isEphemeralMessage(message)) {
|
|
105
|
+
const outbound =
|
|
106
|
+
"count" in message
|
|
107
|
+
? message
|
|
108
|
+
: {
|
|
109
|
+
...message,
|
|
110
|
+
count: ++this.#count,
|
|
111
|
+
sessionId: this.#sessionId,
|
|
112
|
+
senderId: this.peerId,
|
|
113
|
+
}
|
|
114
|
+
this.#log("Ephemeral message", outbound)
|
|
115
|
+
peer.send(outbound)
|
|
89
116
|
} else {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
this.#log(`Sending message to ${peerId}`)
|
|
96
|
-
peer.sendMessage(peerId, channelId, message, false)
|
|
117
|
+
const outbound = { ...message, senderId: this.peerId }
|
|
118
|
+
this.#log("Sync message", outbound)
|
|
119
|
+
peer.send(outbound)
|
|
97
120
|
}
|
|
98
121
|
}
|
|
99
122
|
|
|
@@ -117,7 +140,7 @@ function randomPeerId() {
|
|
|
117
140
|
export interface NetworkSubsystemEvents {
|
|
118
141
|
peer: (payload: PeerPayload) => void
|
|
119
142
|
"peer-disconnected": (payload: PeerDisconnectedPayload) => void
|
|
120
|
-
message: (payload:
|
|
143
|
+
message: (payload: Message) => void
|
|
121
144
|
}
|
|
122
145
|
|
|
123
146
|
export interface PeerPayload {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// utilities
|
|
2
|
+
import { SessionId } from "../EphemeralData"
|
|
3
|
+
import { DocumentId, PeerId } from "../types"
|
|
4
|
+
|
|
5
|
+
export function isValidMessage(
|
|
6
|
+
message: NetworkAdapterMessage
|
|
7
|
+
): message is
|
|
8
|
+
| SyncMessage
|
|
9
|
+
| EphemeralMessage
|
|
10
|
+
| RequestMessage
|
|
11
|
+
| DocumentUnavailableMessage {
|
|
12
|
+
return (
|
|
13
|
+
typeof message === "object" &&
|
|
14
|
+
typeof message.type === "string" &&
|
|
15
|
+
typeof message.senderId === "string" &&
|
|
16
|
+
(isSyncMessage(message) ||
|
|
17
|
+
isEphemeralMessage(message) ||
|
|
18
|
+
isRequestMessage(message) ||
|
|
19
|
+
isDocumentUnavailableMessage(message))
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isDocumentUnavailableMessage(
|
|
24
|
+
message: NetworkAdapterMessage
|
|
25
|
+
): message is DocumentUnavailableMessage {
|
|
26
|
+
return message.type === "doc-unavailable"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isRequestMessage(
|
|
30
|
+
message: NetworkAdapterMessage
|
|
31
|
+
): message is RequestMessage {
|
|
32
|
+
return message.type === "request"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isSyncMessage(
|
|
36
|
+
message: NetworkAdapterMessage
|
|
37
|
+
): message is SyncMessage {
|
|
38
|
+
return message.type === "sync"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isEphemeralMessage(
|
|
42
|
+
message: NetworkAdapterMessage | MessageContents
|
|
43
|
+
): message is EphemeralMessage | EphemeralMessageContents {
|
|
44
|
+
return message.type === "ephemeral"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SyncMessageEnvelope {
|
|
48
|
+
senderId: PeerId
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SyncMessageContents {
|
|
52
|
+
type: "sync"
|
|
53
|
+
data: Uint8Array
|
|
54
|
+
targetId: PeerId
|
|
55
|
+
documentId: DocumentId
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type SyncMessage = SyncMessageEnvelope & SyncMessageContents
|
|
59
|
+
|
|
60
|
+
export interface EphemeralMessageEnvelope {
|
|
61
|
+
senderId: PeerId
|
|
62
|
+
count: number
|
|
63
|
+
sessionId: SessionId
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface EphemeralMessageContents {
|
|
67
|
+
type: "ephemeral"
|
|
68
|
+
targetId: PeerId
|
|
69
|
+
documentId: DocumentId
|
|
70
|
+
data: Uint8Array
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type EphemeralMessage = EphemeralMessageEnvelope &
|
|
74
|
+
EphemeralMessageContents
|
|
75
|
+
|
|
76
|
+
export interface DocumentUnavailableMessageContents {
|
|
77
|
+
type: "doc-unavailable"
|
|
78
|
+
documentId: DocumentId
|
|
79
|
+
targetId: PeerId
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type DocumentUnavailableMessage = SyncMessageEnvelope &
|
|
83
|
+
DocumentUnavailableMessageContents
|
|
84
|
+
|
|
85
|
+
export interface RequestMessageContents {
|
|
86
|
+
type: "request"
|
|
87
|
+
data: Uint8Array
|
|
88
|
+
targetId: PeerId
|
|
89
|
+
documentId: DocumentId
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type RequestMessage = SyncMessageEnvelope & RequestMessageContents
|
|
93
|
+
|
|
94
|
+
export type MessageContents =
|
|
95
|
+
| SyncMessageContents
|
|
96
|
+
| EphemeralMessageContents
|
|
97
|
+
| RequestMessageContents
|
|
98
|
+
| DocumentUnavailableMessageContents
|
|
99
|
+
|
|
100
|
+
export type Message =
|
|
101
|
+
| SyncMessage
|
|
102
|
+
| EphemeralMessage
|
|
103
|
+
| RequestMessage
|
|
104
|
+
| DocumentUnavailableMessage
|
|
105
|
+
|
|
106
|
+
export type SynchronizerMessage =
|
|
107
|
+
| SyncMessage
|
|
108
|
+
| RequestMessage
|
|
109
|
+
| DocumentUnavailableMessage
|
|
110
|
+
| EphemeralMessage
|
|
111
|
+
|
|
112
|
+
type ArriveMessage = {
|
|
113
|
+
senderId: PeerId
|
|
114
|
+
type: "arrive"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type WelcomeMessage = {
|
|
118
|
+
senderId: PeerId
|
|
119
|
+
targetId: PeerId
|
|
120
|
+
type: "welcome"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type NetworkAdapterMessage = ArriveMessage | WelcomeMessage | Message
|