@comapeo/core 3.0.0-0 → 3.1.0
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/generated/rpc.d.ts +47 -0
- package/dist/generated/rpc.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/invite/invite-api.d.ts +4 -5
- package/dist/invite/invite-api.d.ts.map +1 -1
- package/dist/local-peers.d.ts +31 -0
- package/dist/local-peers.d.ts.map +1 -1
- package/dist/mapeo-manager.d.ts +30 -22
- package/dist/mapeo-manager.d.ts.map +1 -1
- package/dist/mapeo-project.d.ts +39 -1
- package/dist/mapeo-project.d.ts.map +1 -1
- package/dist/member-api.d.ts +15 -1
- package/dist/member-api.d.ts.map +1 -1
- package/dist/schema/client.d.ts +26 -3
- package/dist/schema/client.d.ts.map +1 -1
- package/dist/sync/sync-api.d.ts +4 -1
- package/dist/sync/sync-api.d.ts.map +1 -1
- package/drizzle/client/0002_brief_demogoblin.sql +2 -0
- package/drizzle/client/meta/0002_snapshot.json +220 -0
- package/drizzle/client/meta/_journal.json +7 -0
- package/package.json +3 -3
- package/src/generated/rpc.d.ts +47 -0
- package/src/generated/rpc.js +241 -3
- package/src/generated/rpc.ts +280 -1
- package/src/invite/invite-api.js +15 -3
- package/src/local-peers.js +258 -21
- package/src/mapeo-manager.js +60 -20
- package/src/mapeo-project.js +21 -3
- package/src/member-api.js +67 -10
- package/src/schema/client.js +3 -2
- package/src/sync/sync-api.js +6 -2
- package/dist/blob-store/live-download.d.ts +0 -107
- package/dist/blob-store/live-download.d.ts.map +0 -1
- package/dist/capabilities.d.ts +0 -121
- package/dist/capabilities.d.ts.map +0 -1
- package/dist/core-manager/compat.d.ts +0 -4
- package/dist/core-manager/compat.d.ts.map +0 -1
- package/dist/discovery/dns-sd.d.ts +0 -54
- package/dist/discovery/dns-sd.d.ts.map +0 -1
- package/dist/fastify-plugins/maps/index.d.ts +0 -11
- package/dist/fastify-plugins/maps/index.d.ts.map +0 -1
- package/dist/fastify-plugins/maps/offline-fallback-map.d.ts +0 -12
- package/dist/fastify-plugins/maps/offline-fallback-map.d.ts.map +0 -1
- package/dist/fastify-plugins/maps/static-maps.d.ts +0 -11
- package/dist/fastify-plugins/maps/static-maps.d.ts.map +0 -1
- package/dist/invite-api.d.ts +0 -70
- package/dist/invite-api.d.ts.map +0 -1
- package/dist/lib/timing-safe-equal.d.ts +0 -15
- package/dist/lib/timing-safe-equal.d.ts.map +0 -1
- package/dist/media-server.d.ts +0 -36
- package/dist/media-server.d.ts.map +0 -1
- package/dist/server/ws-core-replicator.d.ts +0 -6
- package/dist/server/ws-core-replicator.d.ts.map +0 -1
package/src/local-peers.js
CHANGED
|
@@ -1,20 +1,43 @@
|
|
|
1
1
|
import { TypedEmitter } from 'tiny-typed-emitter'
|
|
2
2
|
import Protomux from 'protomux'
|
|
3
|
+
import timingSafeEqual from 'string-timing-safe-equal'
|
|
3
4
|
import { assert, ExhaustivenessError, keyToId, noop } from './utils.js'
|
|
4
5
|
import { isBlank } from './lib/string.js'
|
|
5
6
|
import cenc from 'compact-encoding'
|
|
6
7
|
import {
|
|
7
8
|
DeviceInfo,
|
|
8
9
|
Invite,
|
|
10
|
+
InviteAck,
|
|
9
11
|
InviteCancel,
|
|
12
|
+
InviteCancelAck,
|
|
10
13
|
InviteResponse,
|
|
14
|
+
InviteResponseAck,
|
|
11
15
|
ProjectJoinDetails,
|
|
16
|
+
ProjectJoinDetailsAck,
|
|
17
|
+
DeviceInfo_RPCFeatures,
|
|
12
18
|
} from './generated/rpc.js'
|
|
13
19
|
import pDefer from 'p-defer'
|
|
14
20
|
import { Logger } from './logger.js'
|
|
15
21
|
import pTimeout, { TimeoutError } from 'p-timeout'
|
|
16
22
|
/** @import NoiseStream from '@hyperswarm/secret-stream' */
|
|
17
23
|
/** @import { OpenedNoiseStream } from './lib/noise-secret-stream-helpers.js' */
|
|
24
|
+
/** @import {DeferredPromise} from 'p-defer' */
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {InviteAck|InviteCancelAck|InviteResponseAck|ProjectJoinDetailsAck} AckResponse
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @callback AckFilter
|
|
32
|
+
* @param {AckResponse} ack
|
|
33
|
+
* @returns {boolean}
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} AckWaiter
|
|
38
|
+
* @property {AckFilter} filter
|
|
39
|
+
* @property {DeferredPromise<void>} deferred
|
|
40
|
+
*/
|
|
18
41
|
|
|
19
42
|
// Unique identifier for the mapeo rpc protocol
|
|
20
43
|
const PROTOCOL_NAME = 'mapeo/rpc'
|
|
@@ -33,6 +56,10 @@ const MESSAGE_TYPES = {
|
|
|
33
56
|
InviteResponse: 2,
|
|
34
57
|
ProjectJoinDetails: 3,
|
|
35
58
|
DeviceInfo: 4,
|
|
59
|
+
InviteAck: 5,
|
|
60
|
+
InviteCancelAck: 6,
|
|
61
|
+
InviteResponseAck: 7,
|
|
62
|
+
ProjectJoinDetailsAck: 8,
|
|
36
63
|
}
|
|
37
64
|
const MESSAGES_MAX_ID = Math.max.apply(null, [...Object.values(MESSAGE_TYPES)])
|
|
38
65
|
|
|
@@ -62,8 +89,14 @@ class Peer {
|
|
|
62
89
|
#name
|
|
63
90
|
/** @type {DeviceInfo['deviceType']} */
|
|
64
91
|
#deviceType
|
|
92
|
+
/** @type {DeviceInfo['features']} */
|
|
93
|
+
#features = []
|
|
65
94
|
#connectedAt = 0
|
|
66
95
|
#disconnectedAt = 0
|
|
96
|
+
#drainedListeners = new Set()
|
|
97
|
+
// Map of type -> Set<{filter, deferred}>
|
|
98
|
+
/** @type Map<keyof typeof MESSAGE_TYPES, Set<AckWaiter>>*/
|
|
99
|
+
#ackWaiters = new Map()
|
|
67
100
|
#protomux
|
|
68
101
|
#log
|
|
69
102
|
|
|
@@ -157,61 +190,218 @@ class Peer {
|
|
|
157
190
|
this.#disconnectedAt = Date.now()
|
|
158
191
|
// This promise should have already resolved, but if the peer never connected then we reject here
|
|
159
192
|
this.#connected.reject(new PeerFailedConnectionError())
|
|
193
|
+
for (const listener of this.#drainedListeners) {
|
|
194
|
+
listener.reject(new Error('RPC Disconnected before sending'))
|
|
195
|
+
}
|
|
196
|
+
for (const waiters of this.#ackWaiters.values()) {
|
|
197
|
+
for (const { deferred } of waiters) {
|
|
198
|
+
deferred.reject(new Error('RPC disconnected before receiving ACK'))
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
this.#ackWaiters.clear()
|
|
202
|
+
this.#drainedListeners.clear()
|
|
160
203
|
this.#log('disconnected')
|
|
161
204
|
}
|
|
205
|
+
|
|
206
|
+
// Call this when the stream has drained all data to the network
|
|
207
|
+
drained() {
|
|
208
|
+
for (const listener of this.#drainedListeners) {
|
|
209
|
+
listener.resolve()
|
|
210
|
+
}
|
|
211
|
+
this.#drainedListeners.clear()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {boolean} didWrite
|
|
216
|
+
* @returns {Promise<void>}
|
|
217
|
+
*/
|
|
218
|
+
async #waitForDrain(didWrite) {
|
|
219
|
+
if (didWrite) return
|
|
220
|
+
const onDrain = pDefer()
|
|
221
|
+
|
|
222
|
+
this.#drainedListeners.add(onDrain)
|
|
223
|
+
|
|
224
|
+
await onDrain.promise
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Check if RPC Acknowledgement messages are supported by this peer
|
|
229
|
+
* @returns {boolean}
|
|
230
|
+
*/
|
|
231
|
+
supportsAck() {
|
|
232
|
+
return this.#features.includes(DeviceInfo_RPCFeatures.ack) ?? false
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {keyof typeof MESSAGE_TYPES} type
|
|
237
|
+
* @param {AckFilter} filter
|
|
238
|
+
* @returns {Promise<void>}
|
|
239
|
+
*/
|
|
240
|
+
async #waitForAck(type, filter) {
|
|
241
|
+
if (!this.supportsAck()) return
|
|
242
|
+
if (!this.#ackWaiters.has(type)) {
|
|
243
|
+
this.#ackWaiters.set(type, new Set())
|
|
244
|
+
}
|
|
245
|
+
const deferred = pDefer()
|
|
246
|
+
this.#ackWaiters.get(type)?.add({
|
|
247
|
+
deferred,
|
|
248
|
+
filter,
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
await deferred.promise
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {keyof typeof MESSAGE_TYPES} type
|
|
256
|
+
* @param {AckResponse} ack
|
|
257
|
+
*/
|
|
258
|
+
receiveAck(type, ack) {
|
|
259
|
+
if (!this.supportsAck()) return
|
|
260
|
+
if (!this.#ackWaiters.has(type)) return
|
|
261
|
+
const waiters = this.#ackWaiters.get(type)
|
|
262
|
+
if (!waiters || !waiters.size) {
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const waiter of waiters) {
|
|
267
|
+
if (waiter.filter(ack)) {
|
|
268
|
+
waiter.deferred.resolve()
|
|
269
|
+
waiters.delete(waiter)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
162
274
|
/**
|
|
163
275
|
* @param {Buffer} buf
|
|
276
|
+
* @returns {Promise<void>}
|
|
164
277
|
*/
|
|
165
|
-
[kTestOnlySendRawInvite](buf) {
|
|
278
|
+
async [kTestOnlySendRawInvite](buf) {
|
|
166
279
|
this.#assertConnected()
|
|
167
280
|
const messageType = MESSAGE_TYPES.Invite
|
|
168
|
-
this.#channel.messages[messageType].send(buf)
|
|
281
|
+
await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
|
|
169
282
|
}
|
|
170
|
-
/**
|
|
171
|
-
|
|
283
|
+
/**
|
|
284
|
+
* @param {Invite} invite
|
|
285
|
+
* @returns {Promise<void>}
|
|
286
|
+
*/
|
|
287
|
+
async sendInvite(invite) {
|
|
172
288
|
this.#assertConnected('Peer disconnected before sending invite')
|
|
173
289
|
const buf = Buffer.from(Invite.encode(invite).finish())
|
|
174
290
|
const messageType = MESSAGE_TYPES.Invite
|
|
175
|
-
this.#channel.messages[messageType].send(buf)
|
|
291
|
+
await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
|
|
292
|
+
await this.#waitForAck('InviteAck', ({ inviteId }) =>
|
|
293
|
+
timingSafeEqual(inviteId, invite.inviteId)
|
|
294
|
+
)
|
|
176
295
|
this.#log('sent invite %h', invite.inviteId)
|
|
177
296
|
}
|
|
178
|
-
|
|
179
|
-
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @param {Invite} invite
|
|
300
|
+
* @returns {Promise<void>}
|
|
301
|
+
*/
|
|
302
|
+
async sendInviteAck({ inviteId }) {
|
|
303
|
+
this.#assertConnected('Peer disconnected before sending invite ack')
|
|
304
|
+
if (!this.supportsAck()) return
|
|
305
|
+
const buf = Buffer.from(InviteAck.encode({ inviteId }).finish())
|
|
306
|
+
const messageType = MESSAGE_TYPES.InviteAck
|
|
307
|
+
await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* @param {InviteCancel} inviteCancel
|
|
312
|
+
* @returns {Promise<void>}
|
|
313
|
+
*/
|
|
314
|
+
async sendInviteCancel(inviteCancel) {
|
|
180
315
|
this.#assertConnected('Peer disconnected before sending invite cancel')
|
|
181
316
|
const buf = Buffer.from(InviteCancel.encode(inviteCancel).finish())
|
|
182
317
|
const messageType = MESSAGE_TYPES.InviteCancel
|
|
183
|
-
this.#channel.messages[messageType].send(buf)
|
|
318
|
+
await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
|
|
319
|
+
await this.#waitForAck('InviteCancelAck', ({ inviteId }) =>
|
|
320
|
+
timingSafeEqual(inviteId, inviteCancel.inviteId)
|
|
321
|
+
)
|
|
184
322
|
this.#log('sent invite cancel %h', inviteCancel.inviteId)
|
|
185
323
|
}
|
|
186
|
-
|
|
187
|
-
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @param {InviteCancel} inviteCancel
|
|
327
|
+
* @returns {Promise<void>}
|
|
328
|
+
*/
|
|
329
|
+
async sendInviteCancelAck({ inviteId }) {
|
|
330
|
+
this.#assertConnected('Peer disconnected before sending invite cancel ack')
|
|
331
|
+
if (!this.supportsAck()) return
|
|
332
|
+
const buf = Buffer.from(InviteCancelAck.encode({ inviteId }).finish())
|
|
333
|
+
const messageType = MESSAGE_TYPES.InviteCancelAck
|
|
334
|
+
await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* @param {InviteResponse} response
|
|
339
|
+
* @returns {Promise<void>}
|
|
340
|
+
*/
|
|
341
|
+
async sendInviteResponse(response) {
|
|
188
342
|
this.#assertConnected('Peer disconnected before sending invite response')
|
|
189
343
|
const buf = Buffer.from(InviteResponse.encode(response).finish())
|
|
190
344
|
const messageType = MESSAGE_TYPES.InviteResponse
|
|
191
|
-
this.#channel.messages[messageType].send(buf)
|
|
345
|
+
await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
|
|
346
|
+
await this.#waitForAck('InviteResponseAck', ({ inviteId }) =>
|
|
347
|
+
timingSafeEqual(inviteId, response.inviteId)
|
|
348
|
+
)
|
|
192
349
|
this.#log('sent response for %h: %s', response.inviteId, response.decision)
|
|
193
350
|
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* @param {InviteResponse} response
|
|
354
|
+
* @returns {Promise<void>}
|
|
355
|
+
*/
|
|
356
|
+
async sendInviteResponseAck({ inviteId }) {
|
|
357
|
+
this.#assertConnected(
|
|
358
|
+
'Peer disconnected before sending invite response ack'
|
|
359
|
+
)
|
|
360
|
+
if (!this.supportsAck()) return
|
|
361
|
+
const buf = Buffer.from(InviteResponseAck.encode({ inviteId }).finish())
|
|
362
|
+
const messageType = MESSAGE_TYPES.InviteResponseAck
|
|
363
|
+
await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
|
|
364
|
+
}
|
|
365
|
+
|
|
194
366
|
/** @param {ProjectJoinDetails} details */
|
|
195
|
-
sendProjectJoinDetails(details) {
|
|
367
|
+
async sendProjectJoinDetails(details) {
|
|
196
368
|
this.#assertConnected(
|
|
197
369
|
'Peer disconnected before sending project join details'
|
|
198
370
|
)
|
|
199
371
|
const buf = Buffer.from(ProjectJoinDetails.encode(details).finish())
|
|
200
372
|
const messageType = MESSAGE_TYPES.ProjectJoinDetails
|
|
201
|
-
this.#channel.messages[messageType].send(buf)
|
|
373
|
+
await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
|
|
374
|
+
await this.#waitForAck('ProjectJoinDetailsAck', ({ inviteId }) =>
|
|
375
|
+
timingSafeEqual(inviteId, details.inviteId)
|
|
376
|
+
)
|
|
202
377
|
this.#log('sent project join details for %h', details.projectKey)
|
|
203
378
|
}
|
|
204
|
-
/** @param {
|
|
205
|
-
|
|
379
|
+
/** @param {ProjectJoinDetails} details */
|
|
380
|
+
async sendProjectJoinDetailsAck({ inviteId }) {
|
|
381
|
+
this.#assertConnected(
|
|
382
|
+
'Peer disconnected before sending project join details ack'
|
|
383
|
+
)
|
|
384
|
+
if (!this.supportsAck()) return
|
|
385
|
+
const buf = Buffer.from(ProjectJoinDetailsAck.encode({ inviteId }).finish())
|
|
386
|
+
const messageType = MESSAGE_TYPES.ProjectJoinDetailsAck
|
|
387
|
+
await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {DeviceInfo} deviceInfo
|
|
392
|
+
* @returns {Promise<void>}
|
|
393
|
+
*/
|
|
394
|
+
async sendDeviceInfo(deviceInfo) {
|
|
206
395
|
const buf = Buffer.from(DeviceInfo.encode(deviceInfo).finish())
|
|
207
396
|
const messageType = MESSAGE_TYPES.DeviceInfo
|
|
208
|
-
this.#channel.messages[messageType].send(buf)
|
|
397
|
+
await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
|
|
209
398
|
this.#log('sent deviceInfo %o', deviceInfo)
|
|
210
399
|
}
|
|
211
400
|
/** @param {DeviceInfo} deviceInfo */
|
|
212
401
|
receiveDeviceInfo(deviceInfo) {
|
|
213
402
|
this.#name = deviceInfo.name
|
|
214
403
|
this.#deviceType = deviceInfo.deviceType
|
|
404
|
+
this.#features = deviceInfo.features
|
|
215
405
|
this.#log('received deviceInfo %o', deviceInfo)
|
|
216
406
|
}
|
|
217
407
|
/** @param {string} [message] */
|
|
@@ -227,9 +417,13 @@ class Peer {
|
|
|
227
417
|
* @property {(peers: PeerInfo[]) => void} peers Emitted whenever the connection status of peers changes. An array of peerInfo objects with a peer id and the peer connection status
|
|
228
418
|
* @property {(peer: PeerInfoConnected) => void} peer-add Emitted when a new peer is connected
|
|
229
419
|
* @property {(peerId: string, invite: Invite) => void} invite Emitted when an invite is received
|
|
420
|
+
* @property {(peerId: string, invite: InviteAck) => void} invite-ack Emitted when an invite acknowledgement is received
|
|
230
421
|
* @property {(peerId: string, invite: InviteCancel) => void} invite-cancel Emitted when we receive a cancelation for an invite
|
|
422
|
+
* @property {(peerId: string, invite: InviteCancelAck) => void} invite-cancel-ack Emitted when we receive a cancelation acknowledgement for an invite
|
|
231
423
|
* @property {(peerId: string, inviteResponse: InviteResponse) => void} invite-response Emitted when an invite response is received
|
|
424
|
+
* @property {(peerId: string, inviteResponse: InviteResponseAck) => void} invite-response-ack Emitted when an invite response acknowledgement is received
|
|
232
425
|
* @property {(peerId: string, details: ProjectJoinDetails) => void} got-project-details Emitted when project details are received
|
|
426
|
+
* @property {(peerId: string, details: ProjectJoinDetailsAck) => void} got-project-details-ack Emitted when project details are acknowledged as received
|
|
233
427
|
* @property {(discoveryKey: Buffer, protomux: Protomux<import('@hyperswarm/secret-stream')>) => void} discovery-key Emitted when a new hypercore is replicated (by a peer) to a peer protomux instance (passed as the second parameter)
|
|
234
428
|
* @property {(messageType: string, errorMessage?: string) => void} failed-to-handle-message Emitted when we received a message we couldn't handle for some reason. Primarily useful for testing
|
|
235
429
|
*/
|
|
@@ -273,7 +467,7 @@ export class LocalPeers extends TypedEmitter {
|
|
|
273
467
|
async sendInvite(deviceId, invite) {
|
|
274
468
|
await this.#waitForPendingConnections()
|
|
275
469
|
const peer = await this.#getPeerByDeviceId(deviceId)
|
|
276
|
-
peer.sendInvite(invite)
|
|
470
|
+
await peer.sendInvite(invite)
|
|
277
471
|
}
|
|
278
472
|
|
|
279
473
|
/**
|
|
@@ -284,7 +478,7 @@ export class LocalPeers extends TypedEmitter {
|
|
|
284
478
|
async sendInviteCancel(deviceId, inviteCancel) {
|
|
285
479
|
await this.#waitForPendingConnections()
|
|
286
480
|
const peer = await this.#getPeerByDeviceId(deviceId)
|
|
287
|
-
peer.sendInviteCancel(inviteCancel)
|
|
481
|
+
await peer.sendInviteCancel(inviteCancel)
|
|
288
482
|
}
|
|
289
483
|
|
|
290
484
|
/**
|
|
@@ -296,7 +490,7 @@ export class LocalPeers extends TypedEmitter {
|
|
|
296
490
|
async sendInviteResponse(deviceId, inviteResponse) {
|
|
297
491
|
await this.#waitForPendingConnections()
|
|
298
492
|
const peer = await this.#getPeerByDeviceId(deviceId)
|
|
299
|
-
peer.sendInviteResponse(inviteResponse)
|
|
493
|
+
await peer.sendInviteResponse(inviteResponse)
|
|
300
494
|
}
|
|
301
495
|
|
|
302
496
|
/**
|
|
@@ -306,7 +500,7 @@ export class LocalPeers extends TypedEmitter {
|
|
|
306
500
|
async sendProjectJoinDetails(deviceId, details) {
|
|
307
501
|
await this.#waitForPendingConnections()
|
|
308
502
|
const peer = await this.#getPeerByDeviceId(deviceId)
|
|
309
|
-
peer.sendProjectJoinDetails(details)
|
|
503
|
+
await peer.sendProjectJoinDetails(details)
|
|
310
504
|
}
|
|
311
505
|
|
|
312
506
|
/**
|
|
@@ -317,7 +511,7 @@ export class LocalPeers extends TypedEmitter {
|
|
|
317
511
|
async sendDeviceInfo(deviceId, deviceInfo) {
|
|
318
512
|
await this.#waitForPendingConnections()
|
|
319
513
|
const peer = await this.#getPeerByDeviceId(deviceId)
|
|
320
|
-
peer.sendDeviceInfo(deviceInfo)
|
|
514
|
+
await peer.sendDeviceInfo(deviceInfo)
|
|
321
515
|
}
|
|
322
516
|
|
|
323
517
|
/**
|
|
@@ -449,6 +643,9 @@ export class LocalPeers extends TypedEmitter {
|
|
|
449
643
|
this.#emitPeers()
|
|
450
644
|
done()
|
|
451
645
|
},
|
|
646
|
+
ondrain: () => {
|
|
647
|
+
peer.drained()
|
|
648
|
+
},
|
|
452
649
|
})
|
|
453
650
|
channel.open()
|
|
454
651
|
|
|
@@ -529,6 +726,9 @@ export class LocalPeers extends TypedEmitter {
|
|
|
529
726
|
const invite = parseInvite(value)
|
|
530
727
|
const peerId = keyToId(protomux.stream.remotePublicKey)
|
|
531
728
|
this.emit('invite', peerId, invite)
|
|
729
|
+
peer.sendInviteAck(invite).catch((e) => {
|
|
730
|
+
this.#l.log(`Error sending invite ack ${e.stack}`)
|
|
731
|
+
})
|
|
532
732
|
this.#l.log(
|
|
533
733
|
'Invite %h from %S for %h',
|
|
534
734
|
invite.inviteId,
|
|
@@ -541,6 +741,9 @@ export class LocalPeers extends TypedEmitter {
|
|
|
541
741
|
const inviteCancel = parseInviteCancel(value)
|
|
542
742
|
const peerId = keyToId(protomux.stream.remotePublicKey)
|
|
543
743
|
this.emit('invite-cancel', peerId, inviteCancel)
|
|
744
|
+
peer.sendInviteCancelAck(inviteCancel).catch((e) => {
|
|
745
|
+
this.#l.log(`Error sending invite cancel ack ${e.stack}`)
|
|
746
|
+
})
|
|
544
747
|
this.#l.log(
|
|
545
748
|
'Invite cancel from %S for %h',
|
|
546
749
|
peerId,
|
|
@@ -552,12 +755,18 @@ export class LocalPeers extends TypedEmitter {
|
|
|
552
755
|
const inviteResponse = parseInviteResponse(value)
|
|
553
756
|
const peerId = keyToId(protomux.stream.remotePublicKey)
|
|
554
757
|
this.emit('invite-response', peerId, inviteResponse)
|
|
758
|
+
peer.sendInviteResponseAck(inviteResponse).catch((e) => {
|
|
759
|
+
this.#l.log(`Error sending invite response ack ${e.stack}`)
|
|
760
|
+
})
|
|
555
761
|
break
|
|
556
762
|
}
|
|
557
763
|
case 'ProjectJoinDetails': {
|
|
558
764
|
const details = parseProjectJoinDetails(value)
|
|
559
765
|
const peerId = keyToId(protomux.stream.remotePublicKey)
|
|
560
766
|
this.emit('got-project-details', peerId, details)
|
|
767
|
+
peer.sendProjectJoinDetailsAck(details).catch((e) => {
|
|
768
|
+
this.#l.log(`Error sending project details ack ${e.stack}`)
|
|
769
|
+
})
|
|
561
770
|
break
|
|
562
771
|
}
|
|
563
772
|
case 'DeviceInfo': {
|
|
@@ -566,6 +775,34 @@ export class LocalPeers extends TypedEmitter {
|
|
|
566
775
|
this.#emitPeers()
|
|
567
776
|
break
|
|
568
777
|
}
|
|
778
|
+
case 'InviteAck': {
|
|
779
|
+
const ack = InviteAck.decode(value)
|
|
780
|
+
peer.receiveAck('InviteAck', ack)
|
|
781
|
+
const peerId = keyToId(protomux.stream.remotePublicKey)
|
|
782
|
+
this.emit('invite-ack', peerId, ack)
|
|
783
|
+
break
|
|
784
|
+
}
|
|
785
|
+
case 'InviteCancelAck': {
|
|
786
|
+
const ack = InviteCancelAck.decode(value)
|
|
787
|
+
peer.receiveAck('InviteCancelAck', ack)
|
|
788
|
+
const peerId = keyToId(protomux.stream.remotePublicKey)
|
|
789
|
+
this.emit('invite-cancel-ack', peerId, ack)
|
|
790
|
+
break
|
|
791
|
+
}
|
|
792
|
+
case 'InviteResponseAck': {
|
|
793
|
+
const ack = InviteResponseAck.decode(value)
|
|
794
|
+
peer.receiveAck('InviteResponseAck', ack)
|
|
795
|
+
const peerId = keyToId(protomux.stream.remotePublicKey)
|
|
796
|
+
this.emit('invite-response-ack', peerId, ack)
|
|
797
|
+
break
|
|
798
|
+
}
|
|
799
|
+
case 'ProjectJoinDetailsAck': {
|
|
800
|
+
const ack = ProjectJoinDetailsAck.decode(value)
|
|
801
|
+
peer.receiveAck('ProjectJoinDetailsAck', ack)
|
|
802
|
+
const peerId = keyToId(protomux.stream.remotePublicKey)
|
|
803
|
+
this.emit('got-project-details-ack', peerId, ack)
|
|
804
|
+
break
|
|
805
|
+
}
|
|
569
806
|
/* c8 ignore next 2 */
|
|
570
807
|
default:
|
|
571
808
|
throw new ExhaustivenessError(type)
|
package/src/mapeo-manager.js
CHANGED
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
projectSettingsTable,
|
|
26
26
|
} from './schema/client.js'
|
|
27
27
|
import { ProjectKeys } from './generated/keys.js'
|
|
28
|
+
import {
|
|
29
|
+
DeviceInfo_RPCFeatures,
|
|
30
|
+
DeviceInfo_DeviceType,
|
|
31
|
+
} from './generated/rpc.js'
|
|
28
32
|
import {
|
|
29
33
|
deNullify,
|
|
30
34
|
getDeviceId,
|
|
@@ -52,13 +56,17 @@ import {
|
|
|
52
56
|
kRescindFullStopRequest,
|
|
53
57
|
} from './sync/sync-api.js'
|
|
54
58
|
import { NotFoundError } from './errors.js'
|
|
55
|
-
|
|
59
|
+
import { WebSocket } from 'ws'
|
|
60
|
+
|
|
56
61
|
/** @import NoiseSecretStream from '@hyperswarm/secret-stream' */
|
|
57
62
|
/** @import { SetNonNullable } from 'type-fest' */
|
|
63
|
+
/** @import { ProjectJoinDetails, } from './generated/rpc.js' */
|
|
58
64
|
/** @import { CoreStorage, Namespace } from './types.js' */
|
|
59
|
-
/** @import { DeviceInfoParam } from './schema/client.js' */
|
|
65
|
+
/** @import { DeviceInfoParam, ProjectInfo } from './schema/client.js' */
|
|
60
66
|
|
|
61
67
|
/** @typedef {SetNonNullable<ProjectKeys, 'encryptionKeys'>} ValidatedProjectKeys */
|
|
68
|
+
/** @typedef {Pick<ProjectJoinDetails, 'projectKey' | 'encryptionKeys'> & { projectName: string, projectColor?: string, projectDescription?: string }} ProjectToAddDetails */
|
|
69
|
+
/** @typedef {{ projectId: string, createdAt?: string, updatedAt?: string, name?: string, projectColor?: string, projectDescription?: string }} ListedProject */
|
|
62
70
|
|
|
63
71
|
const CLIENT_SQLITE_FILE_NAME = 'client.db'
|
|
64
72
|
|
|
@@ -113,6 +121,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
113
121
|
#loggerBase
|
|
114
122
|
#l
|
|
115
123
|
#defaultConfigPath
|
|
124
|
+
#makeWebsocket
|
|
116
125
|
|
|
117
126
|
/**
|
|
118
127
|
* @param {Object} opts
|
|
@@ -126,6 +135,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
126
135
|
* @param {string} [opts.customMapPath] File path to a locally stored Styled Map Package (SMP).
|
|
127
136
|
* @param {string} [opts.fallbackMapPath] File path to a locally stored Styled Map Package (SMP)
|
|
128
137
|
* @param {string} [opts.defaultOnlineStyleUrl] URL for an online-hosted StyleJSON asset.
|
|
138
|
+
* @param {(url: string) => WebSocket} [opts.makeWebsocket]
|
|
129
139
|
*/
|
|
130
140
|
constructor({
|
|
131
141
|
rootKey,
|
|
@@ -138,11 +148,13 @@ export class MapeoManager extends TypedEmitter {
|
|
|
138
148
|
customMapPath,
|
|
139
149
|
fallbackMapPath = DEFAULT_FALLBACK_MAP_FILE_PATH,
|
|
140
150
|
defaultOnlineStyleUrl = DEFAULT_ONLINE_STYLE_URL,
|
|
151
|
+
makeWebsocket = (url) => new WebSocket(url),
|
|
141
152
|
}) {
|
|
142
153
|
super()
|
|
143
154
|
this.#keyManager = new KeyManager(rootKey)
|
|
144
155
|
this.#deviceId = getDeviceId(this.#keyManager)
|
|
145
156
|
this.#defaultConfigPath = defaultConfigPath
|
|
157
|
+
this.#makeWebsocket = makeWebsocket
|
|
146
158
|
const logger = (this.#loggerBase = new Logger({ deviceId: this.#deviceId }))
|
|
147
159
|
this.#l = Logger.create('manager', logger)
|
|
148
160
|
this.#dbFolder = dbFolder
|
|
@@ -270,9 +282,14 @@ export class MapeoManager extends TypedEmitter {
|
|
|
270
282
|
const deviceInfo = this.getDeviceInfo()
|
|
271
283
|
if (!hasSavedDeviceInfo(deviceInfo)) return
|
|
272
284
|
|
|
285
|
+
const deviceInfoToSend = {
|
|
286
|
+
...deviceInfo,
|
|
287
|
+
features: [DeviceInfo_RPCFeatures.ack],
|
|
288
|
+
}
|
|
289
|
+
|
|
273
290
|
const peerId = keyToId(openedNoiseStream.remotePublicKey)
|
|
274
291
|
|
|
275
|
-
return this.#localPeers.sendDeviceInfo(peerId,
|
|
292
|
+
return this.#localPeers.sendDeviceInfo(peerId, deviceInfoToSend)
|
|
276
293
|
})
|
|
277
294
|
.catch((e) => {
|
|
278
295
|
// Ignore error but log
|
|
@@ -318,7 +335,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
318
335
|
* @param {string} opts.projectPublicId
|
|
319
336
|
* @param {Readonly<Buffer>} opts.projectInviteId
|
|
320
337
|
* @param {ProjectKeys} opts.projectKeys
|
|
321
|
-
* @param {Readonly<
|
|
338
|
+
* @param {Readonly<ProjectInfo>} [opts.projectInfo]
|
|
322
339
|
*/
|
|
323
340
|
#saveToProjectKeysTable({
|
|
324
341
|
projectId,
|
|
@@ -353,15 +370,16 @@ export class MapeoManager extends TypedEmitter {
|
|
|
353
370
|
|
|
354
371
|
/**
|
|
355
372
|
* Create a new project.
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
* Partial<Pick<ProjectValue, 'name'>> &
|
|
359
|
-
* { configPath?: string }
|
|
360
|
-
* )>
|
|
361
|
-
* )} [options]
|
|
373
|
+
*
|
|
374
|
+
* @param {{ name?: string, configPath?: string, projectColor?: string, projectDescription?: string }} [options]
|
|
362
375
|
* @returns {Promise<string>} Project public id
|
|
363
376
|
*/
|
|
364
|
-
async createProject({
|
|
377
|
+
async createProject({
|
|
378
|
+
name,
|
|
379
|
+
configPath = this.#defaultConfigPath,
|
|
380
|
+
projectColor,
|
|
381
|
+
projectDescription,
|
|
382
|
+
} = {}) {
|
|
365
383
|
// 1. Create project keypair
|
|
366
384
|
const projectKeypair = KeyManager.generateProjectKeypair()
|
|
367
385
|
|
|
@@ -406,7 +424,11 @@ export class MapeoManager extends TypedEmitter {
|
|
|
406
424
|
})
|
|
407
425
|
|
|
408
426
|
// 5. Write project settings to project instance
|
|
409
|
-
await project.$setProjectSettings({
|
|
427
|
+
await project.$setProjectSettings({
|
|
428
|
+
name,
|
|
429
|
+
projectColor,
|
|
430
|
+
projectDescription,
|
|
431
|
+
})
|
|
410
432
|
|
|
411
433
|
// 6. Write device info into project
|
|
412
434
|
const deviceInfo = this.getDeviceInfo()
|
|
@@ -495,13 +517,14 @@ export class MapeoManager extends TypedEmitter {
|
|
|
495
517
|
logger: this.#loggerBase,
|
|
496
518
|
getMediaBaseUrl: this.#getMediaBaseUrl.bind(this),
|
|
497
519
|
isArchiveDevice,
|
|
520
|
+
makeWebsocket: this.#makeWebsocket,
|
|
498
521
|
})
|
|
499
522
|
await project[kClearDataIfLeft]()
|
|
500
523
|
return project
|
|
501
524
|
}
|
|
502
525
|
|
|
503
526
|
/**
|
|
504
|
-
* @returns {Promise<Array<
|
|
527
|
+
* @returns {Promise<Array<ListedProject>>}
|
|
505
528
|
*/
|
|
506
529
|
async listProjects() {
|
|
507
530
|
// We use the project keys table as the source of truth for projects that exist
|
|
@@ -522,11 +545,13 @@ export class MapeoManager extends TypedEmitter {
|
|
|
522
545
|
createdAt: projectSettingsTable.createdAt,
|
|
523
546
|
updatedAt: projectSettingsTable.updatedAt,
|
|
524
547
|
name: projectSettingsTable.name,
|
|
548
|
+
projectColor: projectSettingsTable.projectColor,
|
|
549
|
+
projectDescription: projectSettingsTable.projectDescription,
|
|
525
550
|
})
|
|
526
551
|
.from(projectSettingsTable)
|
|
527
552
|
.all()
|
|
528
553
|
|
|
529
|
-
/** @type {Array<
|
|
554
|
+
/** @type {Array<ListedProject>} */
|
|
530
555
|
const result = []
|
|
531
556
|
|
|
532
557
|
for (const {
|
|
@@ -544,6 +569,11 @@ export class MapeoManager extends TypedEmitter {
|
|
|
544
569
|
createdAt: existingProject?.createdAt,
|
|
545
570
|
updatedAt: existingProject?.updatedAt,
|
|
546
571
|
name: existingProject?.name || projectInfo.name,
|
|
572
|
+
projectColor:
|
|
573
|
+
existingProject?.projectColor || projectInfo.projectColor,
|
|
574
|
+
projectDescription:
|
|
575
|
+
existingProject?.projectDescription ||
|
|
576
|
+
projectInfo.projectDescription,
|
|
547
577
|
})
|
|
548
578
|
)
|
|
549
579
|
}
|
|
@@ -556,12 +586,18 @@ export class MapeoManager extends TypedEmitter {
|
|
|
556
586
|
* await `project.$waitForInitialSync()` to ensure that the device has
|
|
557
587
|
* downloaded their proof of project membership and the project config.
|
|
558
588
|
*
|
|
559
|
-
* @param {
|
|
589
|
+
* @param {ProjectToAddDetails} projectToAddDetails
|
|
560
590
|
* @param {{ waitForSync?: boolean }} [opts] Set opts.waitForSync = false to not wait for sync during addProject()
|
|
561
591
|
* @returns {Promise<string>}
|
|
562
592
|
*/
|
|
563
593
|
addProject = async (
|
|
564
|
-
{
|
|
594
|
+
{
|
|
595
|
+
projectKey,
|
|
596
|
+
encryptionKeys,
|
|
597
|
+
projectName,
|
|
598
|
+
projectColor,
|
|
599
|
+
projectDescription,
|
|
600
|
+
},
|
|
565
601
|
{ waitForSync = true } = {}
|
|
566
602
|
) => {
|
|
567
603
|
const projectPublicId = projectKeyToPublicId(projectKey)
|
|
@@ -599,7 +635,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
599
635
|
projectKey,
|
|
600
636
|
encryptionKeys,
|
|
601
637
|
},
|
|
602
|
-
projectInfo: { name: projectName },
|
|
638
|
+
projectInfo: { name: projectName, projectColor, projectDescription },
|
|
603
639
|
})
|
|
604
640
|
|
|
605
641
|
// Any errors from here we need to remove project from db because it has not
|
|
@@ -720,7 +756,10 @@ export class MapeoManager extends TypedEmitter {
|
|
|
720
756
|
* @param {T} deviceInfo
|
|
721
757
|
*/
|
|
722
758
|
async setDeviceInfo(deviceInfo) {
|
|
723
|
-
const values = {
|
|
759
|
+
const values = {
|
|
760
|
+
deviceId: this.#deviceId,
|
|
761
|
+
deviceInfo,
|
|
762
|
+
}
|
|
724
763
|
this.#db
|
|
725
764
|
.insert(deviceSettingsTable)
|
|
726
765
|
.values(values)
|
|
@@ -747,6 +786,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
747
786
|
const deviceInfoToSend = {
|
|
748
787
|
...deviceInfo,
|
|
749
788
|
deviceType,
|
|
789
|
+
features: [DeviceInfo_RPCFeatures.ack],
|
|
750
790
|
}
|
|
751
791
|
await Promise.all(
|
|
752
792
|
this.#localPeers.peers
|
|
@@ -764,7 +804,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
764
804
|
* @returns {(
|
|
765
805
|
* {
|
|
766
806
|
* deviceId: string;
|
|
767
|
-
* deviceType: DeviceInfoParam['deviceType']
|
|
807
|
+
* deviceType: DeviceInfoParam['deviceType'];
|
|
768
808
|
* } & Partial<DeviceInfoParam>
|
|
769
809
|
* )}
|
|
770
810
|
*/
|
|
@@ -776,7 +816,7 @@ export class MapeoManager extends TypedEmitter {
|
|
|
776
816
|
.get()
|
|
777
817
|
return {
|
|
778
818
|
deviceId: this.#deviceId,
|
|
779
|
-
deviceType:
|
|
819
|
+
deviceType: DeviceInfo_DeviceType.device_type_unspecified,
|
|
780
820
|
...row?.deviceInfo,
|
|
781
821
|
}
|
|
782
822
|
}
|