@comapeo/core 2.3.2 → 3.0.0-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/blob-store/live-download.d.ts +107 -0
- package/dist/blob-store/live-download.d.ts.map +1 -0
- package/dist/capabilities.d.ts +121 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/core-manager/compat.d.ts +4 -0
- package/dist/core-manager/compat.d.ts.map +1 -0
- package/dist/discovery/dns-sd.d.ts +54 -0
- package/dist/discovery/dns-sd.d.ts.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/fastify-plugins/maps/index.d.ts +11 -0
- package/dist/fastify-plugins/maps/index.d.ts.map +1 -0
- package/dist/fastify-plugins/maps/offline-fallback-map.d.ts +12 -0
- package/dist/fastify-plugins/maps/offline-fallback-map.d.ts.map +1 -0
- package/dist/fastify-plugins/maps/static-maps.d.ts +11 -0
- package/dist/fastify-plugins/maps/static-maps.d.ts.map +1 -0
- package/dist/invite/invite-api.d.ts +112 -0
- package/dist/invite/invite-api.d.ts.map +1 -0
- package/dist/invite/invite-state-machine.d.ts +510 -0
- package/dist/invite/invite-state-machine.d.ts.map +1 -0
- package/dist/lib/timing-safe-equal.d.ts +15 -0
- package/dist/lib/timing-safe-equal.d.ts.map +1 -0
- package/dist/local-peers.d.ts.map +1 -1
- package/dist/mapeo-manager.d.ts +1 -1
- package/dist/mapeo-manager.d.ts.map +1 -1
- package/dist/media-server.d.ts +36 -0
- package/dist/media-server.d.ts.map +1 -0
- package/dist/member-api.d.ts.map +1 -1
- package/dist/server/ws-core-replicator.d.ts +6 -0
- package/dist/server/ws-core-replicator.d.ts.map +1 -0
- package/dist/sync/sync-api.d.ts.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +2 -2
- package/dist/utils.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/errors.js +33 -0
- package/src/invite/StateDiagram.md +47 -0
- package/src/invite/invite-api.js +387 -0
- package/src/invite/invite-state-machine.js +208 -0
- package/src/local-peers.js +12 -9
- package/src/mapeo-manager.js +1 -1
- package/src/member-api.js +5 -4
- package/src/types.ts +6 -0
- package/src/utils.js +8 -3
- package/src/invite-api.js +0 -450
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { setup, assign, fromPromise, assertEvent, raise } from 'xstate'
|
|
2
|
+
import { omit } from '../lib/omit.js'
|
|
3
|
+
import { InviteResponse_Decision } from '../generated/rpc.js'
|
|
4
|
+
import ensureError from 'ensure-error'
|
|
5
|
+
import { TimeoutError } from '../errors.js'
|
|
6
|
+
|
|
7
|
+
const RECEIVE_PROJECT_DETAILS_TIMEOUT_MS = 10_000
|
|
8
|
+
const ADD_PROJECT_TIMEOUT_MS = 10_000
|
|
9
|
+
|
|
10
|
+
/** @import { StringToTaggedUnion } from '../types.js' */
|
|
11
|
+
/** @import { ProjectJoinDetails } from '../generated/rpc.js' */
|
|
12
|
+
/**
|
|
13
|
+
* @internal
|
|
14
|
+
* @typedef {object} Context
|
|
15
|
+
* @property {null | Error} error
|
|
16
|
+
* @property {null | string} projectPublicId
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} MachineSetupTypes
|
|
20
|
+
* @property {Context} context
|
|
21
|
+
* @property {{ projectPublicId: string | null }} output
|
|
22
|
+
* @property {StringToTaggedUnion<'ACCEPT_INVITE' | 'CANCEL_INVITE' | 'REJECT_INVITE' | 'ALREADY_IN_PROJECT' | 'ADDED_PROJECT' | 'PEER_DISCONNECTED'> | ({ type: 'RECEIVE_PROJECT_DETAILS' } & ProjectJoinDetails)} events
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const inviteStateMachine = setup({
|
|
26
|
+
types: /** @type {MachineSetupTypes} */ ({}),
|
|
27
|
+
actors: {
|
|
28
|
+
sendInviteResponse: fromPromise(
|
|
29
|
+
/**
|
|
30
|
+
* @param {{ input: { decision: InviteResponse_Decision }}} _opts
|
|
31
|
+
*/
|
|
32
|
+
async (_opts) => {}
|
|
33
|
+
),
|
|
34
|
+
addProject: fromPromise(
|
|
35
|
+
/**
|
|
36
|
+
* @param {{ input: ProjectJoinDetails }} _opts
|
|
37
|
+
* @returns {Promise<string>} The project public ID
|
|
38
|
+
*/
|
|
39
|
+
async (_opts) => ''
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
guards: {
|
|
43
|
+
isNotAlreadyJoiningOrInProject: () => true,
|
|
44
|
+
},
|
|
45
|
+
delays: {
|
|
46
|
+
receiveTimeout: RECEIVE_PROJECT_DETAILS_TIMEOUT_MS,
|
|
47
|
+
addProjectTimeout: ADD_PROJECT_TIMEOUT_MS,
|
|
48
|
+
},
|
|
49
|
+
}).createMachine({
|
|
50
|
+
id: 'invite',
|
|
51
|
+
context: {
|
|
52
|
+
error: null,
|
|
53
|
+
projectPublicId: null,
|
|
54
|
+
},
|
|
55
|
+
initial: 'pending',
|
|
56
|
+
states: {
|
|
57
|
+
pending: {
|
|
58
|
+
description: 'Pending invite awaiting response',
|
|
59
|
+
on: {
|
|
60
|
+
CANCEL_INVITE: { target: 'canceled' },
|
|
61
|
+
ACCEPT_INVITE: [
|
|
62
|
+
{
|
|
63
|
+
target: 'responding.accept',
|
|
64
|
+
guard: { type: 'isNotAlreadyJoiningOrInProject' },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
actions: raise({ type: 'ALREADY_IN_PROJECT' }),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
ALREADY_IN_PROJECT: {
|
|
71
|
+
target: 'responding.already',
|
|
72
|
+
},
|
|
73
|
+
REJECT_INVITE: {
|
|
74
|
+
target: 'responding.reject',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
responding: {
|
|
79
|
+
description: 'Responding to invite',
|
|
80
|
+
initial: 'default',
|
|
81
|
+
on: {
|
|
82
|
+
CANCEL_INVITE: { target: '#invite.canceled' },
|
|
83
|
+
},
|
|
84
|
+
states: {
|
|
85
|
+
default: {
|
|
86
|
+
always: {
|
|
87
|
+
target: '#invite.error',
|
|
88
|
+
actions: assign({ error: () => new TypeError('InvalidState') }),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
accept: {
|
|
92
|
+
description: 'Sending accept response',
|
|
93
|
+
invoke: {
|
|
94
|
+
src: 'sendInviteResponse',
|
|
95
|
+
input: { decision: InviteResponse_Decision.ACCEPT },
|
|
96
|
+
onDone: '#invite.joining',
|
|
97
|
+
onError: '#invite.error',
|
|
98
|
+
},
|
|
99
|
+
on: {
|
|
100
|
+
// It's possible project details could be received before the send
|
|
101
|
+
// response promise resolves (e.g. somehow the peer receives the
|
|
102
|
+
// response and sends the project details before the response is
|
|
103
|
+
// confirmed as sent), so we accept project details at this point
|
|
104
|
+
RECEIVE_PROJECT_DETAILS: {
|
|
105
|
+
target: '#invite.joining.addingProject',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
reject: {
|
|
110
|
+
description: 'Sending reject response',
|
|
111
|
+
invoke: {
|
|
112
|
+
src: 'sendInviteResponse',
|
|
113
|
+
input: { decision: InviteResponse_Decision.REJECT },
|
|
114
|
+
onDone: '#invite.rejected',
|
|
115
|
+
onError: '#invite.error',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
already: {
|
|
119
|
+
description: 'Sending already response',
|
|
120
|
+
invoke: {
|
|
121
|
+
src: 'sendInviteResponse',
|
|
122
|
+
input: { decision: InviteResponse_Decision.ALREADY },
|
|
123
|
+
onDone: '#invite.respondedAlready',
|
|
124
|
+
onError: '#invite.error',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
joining: {
|
|
130
|
+
initial: 'awaitingDetails',
|
|
131
|
+
description: 'Joining project from invite',
|
|
132
|
+
states: {
|
|
133
|
+
awaitingDetails: {
|
|
134
|
+
description: 'Waiting for project details',
|
|
135
|
+
on: {
|
|
136
|
+
RECEIVE_PROJECT_DETAILS: { target: 'addingProject' },
|
|
137
|
+
CANCEL_INVITE: { target: '#invite.canceled' },
|
|
138
|
+
},
|
|
139
|
+
after: {
|
|
140
|
+
receiveTimeout: {
|
|
141
|
+
target: '#invite.error',
|
|
142
|
+
actions: assign({
|
|
143
|
+
error: () =>
|
|
144
|
+
new TimeoutError('Timed out waiting for project details'),
|
|
145
|
+
}),
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
addingProject: {
|
|
150
|
+
description: 'Adding project from invite',
|
|
151
|
+
invoke: {
|
|
152
|
+
src: 'addProject',
|
|
153
|
+
input: ({ event }) => {
|
|
154
|
+
assertEvent(event, 'RECEIVE_PROJECT_DETAILS')
|
|
155
|
+
return omit(event, ['type'])
|
|
156
|
+
},
|
|
157
|
+
onDone: {
|
|
158
|
+
target: '#invite.joined',
|
|
159
|
+
actions: assign({
|
|
160
|
+
projectPublicId: ({ event }) => event.output,
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
onError: '#invite.error',
|
|
164
|
+
},
|
|
165
|
+
after: {
|
|
166
|
+
addProjectTimeout: {
|
|
167
|
+
target: '#invite.error',
|
|
168
|
+
actions: assign({
|
|
169
|
+
error: () => new TimeoutError('Timed out adding project'),
|
|
170
|
+
}),
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
canceled: {
|
|
177
|
+
description: 'The invite has been canceled',
|
|
178
|
+
type: 'final',
|
|
179
|
+
},
|
|
180
|
+
rejected: {
|
|
181
|
+
description: 'Rejected invite',
|
|
182
|
+
type: 'final',
|
|
183
|
+
},
|
|
184
|
+
respondedAlready: {
|
|
185
|
+
description: 'Responded that already in project',
|
|
186
|
+
type: 'final',
|
|
187
|
+
},
|
|
188
|
+
joined: {
|
|
189
|
+
description: 'Successfully joined project',
|
|
190
|
+
type: 'final',
|
|
191
|
+
},
|
|
192
|
+
error: {
|
|
193
|
+
entry: assign({
|
|
194
|
+
error: ({ event, context }) =>
|
|
195
|
+
context.error ||
|
|
196
|
+
ensureError(
|
|
197
|
+
// @ts-expect-error - xstate types are incorrect, for internal events
|
|
198
|
+
// the error property can exist, and ensureError handles event.error
|
|
199
|
+
// being undefined.
|
|
200
|
+
event.error
|
|
201
|
+
),
|
|
202
|
+
}),
|
|
203
|
+
type: 'final',
|
|
204
|
+
description: 'Error joining project',
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
output: ({ context }) => ({ projectPublicId: context.projectPublicId }),
|
|
208
|
+
})
|
package/src/local-peers.js
CHANGED
|
@@ -169,7 +169,7 @@ class Peer {
|
|
|
169
169
|
}
|
|
170
170
|
/** @param {Invite} invite */
|
|
171
171
|
sendInvite(invite) {
|
|
172
|
-
this.#assertConnected()
|
|
172
|
+
this.#assertConnected('Peer disconnected before sending invite')
|
|
173
173
|
const buf = Buffer.from(Invite.encode(invite).finish())
|
|
174
174
|
const messageType = MESSAGE_TYPES.Invite
|
|
175
175
|
this.#channel.messages[messageType].send(buf)
|
|
@@ -177,7 +177,7 @@ class Peer {
|
|
|
177
177
|
}
|
|
178
178
|
/** @param {InviteCancel} inviteCancel */
|
|
179
179
|
sendInviteCancel(inviteCancel) {
|
|
180
|
-
this.#assertConnected()
|
|
180
|
+
this.#assertConnected('Peer disconnected before sending invite cancel')
|
|
181
181
|
const buf = Buffer.from(InviteCancel.encode(inviteCancel).finish())
|
|
182
182
|
const messageType = MESSAGE_TYPES.InviteCancel
|
|
183
183
|
this.#channel.messages[messageType].send(buf)
|
|
@@ -185,7 +185,7 @@ class Peer {
|
|
|
185
185
|
}
|
|
186
186
|
/** @param {InviteResponse} response */
|
|
187
187
|
sendInviteResponse(response) {
|
|
188
|
-
this.#assertConnected()
|
|
188
|
+
this.#assertConnected('Peer disconnected before sending invite response')
|
|
189
189
|
const buf = Buffer.from(InviteResponse.encode(response).finish())
|
|
190
190
|
const messageType = MESSAGE_TYPES.InviteResponse
|
|
191
191
|
this.#channel.messages[messageType].send(buf)
|
|
@@ -193,7 +193,9 @@ class Peer {
|
|
|
193
193
|
}
|
|
194
194
|
/** @param {ProjectJoinDetails} details */
|
|
195
195
|
sendProjectJoinDetails(details) {
|
|
196
|
-
this.#assertConnected(
|
|
196
|
+
this.#assertConnected(
|
|
197
|
+
'Peer disconnected before sending project join details'
|
|
198
|
+
)
|
|
197
199
|
const buf = Buffer.from(ProjectJoinDetails.encode(details).finish())
|
|
198
200
|
const messageType = MESSAGE_TYPES.ProjectJoinDetails
|
|
199
201
|
this.#channel.messages[messageType].send(buf)
|
|
@@ -212,10 +214,11 @@ class Peer {
|
|
|
212
214
|
this.#deviceType = deviceInfo.deviceType
|
|
213
215
|
this.#log('received deviceInfo %o', deviceInfo)
|
|
214
216
|
}
|
|
215
|
-
|
|
217
|
+
/** @param {string} [message] */
|
|
218
|
+
#assertConnected(message) {
|
|
216
219
|
if (this.#state === 'connected' && !this.#channel.closed) return
|
|
217
220
|
/* c8 ignore next */
|
|
218
|
-
throw new PeerDisconnectedError() // TODO: report error - this should not happen
|
|
221
|
+
throw new PeerDisconnectedError(message) // TODO: report error - this should not happen
|
|
219
222
|
}
|
|
220
223
|
}
|
|
221
224
|
|
|
@@ -614,7 +617,7 @@ export { TimeoutError }
|
|
|
614
617
|
|
|
615
618
|
export class UnknownPeerError extends Error {
|
|
616
619
|
/** @param {string} [message] */
|
|
617
|
-
constructor(message) {
|
|
620
|
+
constructor(message = 'UnknownPeerError') {
|
|
618
621
|
super(message)
|
|
619
622
|
this.name = 'UnknownPeerError'
|
|
620
623
|
}
|
|
@@ -622,7 +625,7 @@ export class UnknownPeerError extends Error {
|
|
|
622
625
|
|
|
623
626
|
export class PeerDisconnectedError extends Error {
|
|
624
627
|
/** @param {string} [message] */
|
|
625
|
-
constructor(message) {
|
|
628
|
+
constructor(message = 'Peer disconnected') {
|
|
626
629
|
super(message)
|
|
627
630
|
this.name = 'PeerDisconnectedError'
|
|
628
631
|
}
|
|
@@ -630,7 +633,7 @@ export class PeerDisconnectedError extends Error {
|
|
|
630
633
|
|
|
631
634
|
export class PeerFailedConnectionError extends Error {
|
|
632
635
|
/** @param {string} [message] */
|
|
633
|
-
constructor(message) {
|
|
636
|
+
constructor(message = 'PeerFailedConnectionError') {
|
|
634
637
|
super(message)
|
|
635
638
|
this.name = 'PeerFailedConnectionError'
|
|
636
639
|
}
|
package/src/mapeo-manager.js
CHANGED
|
@@ -42,7 +42,7 @@ import IconServerPlugin from './fastify-plugins/icons.js'
|
|
|
42
42
|
import { plugin as MapServerPlugin } from './fastify-plugins/maps.js'
|
|
43
43
|
import { getFastifyServerAddress } from './fastify-plugins/utils.js'
|
|
44
44
|
import { LocalPeers } from './local-peers.js'
|
|
45
|
-
import { InviteApi } from './invite-api.js'
|
|
45
|
+
import { InviteApi } from './invite/invite-api.js'
|
|
46
46
|
import { LocalDiscovery } from './discovery/local-discovery.js'
|
|
47
47
|
import { Roles } from './roles.js'
|
|
48
48
|
import { Logger } from './logger.js'
|
package/src/member-api.js
CHANGED
|
@@ -17,6 +17,7 @@ import { abortSignalAny } from './lib/ponyfills.js'
|
|
|
17
17
|
import timingSafeEqual from 'string-timing-safe-equal'
|
|
18
18
|
import { isHostnameIpAddress } from './lib/is-hostname-ip-address.js'
|
|
19
19
|
import { ErrorWithCode, getErrorMessage } from './lib/error.js'
|
|
20
|
+
import { InviteAbortedError } from './errors.js'
|
|
20
21
|
import { wsCoreReplicator } from './lib/ws-core-replicator.js'
|
|
21
22
|
import { MEMBER_ROLE_ID, ROLES, isRoleIdForNewInvite } from './roles.js'
|
|
22
23
|
/**
|
|
@@ -212,9 +213,9 @@ export class MemberApi extends TypedEmitter {
|
|
|
212
213
|
* @param {AbortSignal} signal
|
|
213
214
|
*/
|
|
214
215
|
async #sendInviteAndGetResponse(deviceId, invite, signal) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
if (signal.aborted) {
|
|
217
|
+
throw new InviteAbortedError()
|
|
218
|
+
}
|
|
218
219
|
|
|
219
220
|
const abortController = new AbortController()
|
|
220
221
|
|
|
@@ -246,7 +247,7 @@ export class MemberApi extends TypedEmitter {
|
|
|
246
247
|
return await responsePromise
|
|
247
248
|
} catch (err) {
|
|
248
249
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
249
|
-
throw
|
|
250
|
+
throw new InviteAbortedError()
|
|
250
251
|
} else {
|
|
251
252
|
throw err
|
|
252
253
|
}
|
package/src/types.ts
CHANGED
package/src/utils.js
CHANGED
|
@@ -34,11 +34,16 @@ export function noop() {}
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* @param {unknown} condition
|
|
37
|
-
* @param {string}
|
|
37
|
+
* @param {string | Error} messageOrError
|
|
38
38
|
* @returns {asserts condition}
|
|
39
39
|
*/
|
|
40
|
-
export function assert(condition,
|
|
41
|
-
if (
|
|
40
|
+
export function assert(condition, messageOrError) {
|
|
41
|
+
if (condition) return
|
|
42
|
+
if (typeof messageOrError === 'string') {
|
|
43
|
+
throw new Error(messageOrError)
|
|
44
|
+
} else {
|
|
45
|
+
throw messageOrError
|
|
46
|
+
}
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
/**
|