@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.
Files changed (46) hide show
  1. package/dist/blob-store/live-download.d.ts +107 -0
  2. package/dist/blob-store/live-download.d.ts.map +1 -0
  3. package/dist/capabilities.d.ts +121 -0
  4. package/dist/capabilities.d.ts.map +1 -0
  5. package/dist/core-manager/compat.d.ts +4 -0
  6. package/dist/core-manager/compat.d.ts.map +1 -0
  7. package/dist/discovery/dns-sd.d.ts +54 -0
  8. package/dist/discovery/dns-sd.d.ts.map +1 -0
  9. package/dist/errors.d.ts +16 -0
  10. package/dist/errors.d.ts.map +1 -1
  11. package/dist/fastify-plugins/maps/index.d.ts +11 -0
  12. package/dist/fastify-plugins/maps/index.d.ts.map +1 -0
  13. package/dist/fastify-plugins/maps/offline-fallback-map.d.ts +12 -0
  14. package/dist/fastify-plugins/maps/offline-fallback-map.d.ts.map +1 -0
  15. package/dist/fastify-plugins/maps/static-maps.d.ts +11 -0
  16. package/dist/fastify-plugins/maps/static-maps.d.ts.map +1 -0
  17. package/dist/invite/invite-api.d.ts +112 -0
  18. package/dist/invite/invite-api.d.ts.map +1 -0
  19. package/dist/invite/invite-state-machine.d.ts +510 -0
  20. package/dist/invite/invite-state-machine.d.ts.map +1 -0
  21. package/dist/lib/timing-safe-equal.d.ts +15 -0
  22. package/dist/lib/timing-safe-equal.d.ts.map +1 -0
  23. package/dist/local-peers.d.ts.map +1 -1
  24. package/dist/mapeo-manager.d.ts +1 -1
  25. package/dist/mapeo-manager.d.ts.map +1 -1
  26. package/dist/media-server.d.ts +36 -0
  27. package/dist/media-server.d.ts.map +1 -0
  28. package/dist/member-api.d.ts.map +1 -1
  29. package/dist/server/ws-core-replicator.d.ts +6 -0
  30. package/dist/server/ws-core-replicator.d.ts.map +1 -0
  31. package/dist/sync/sync-api.d.ts.map +1 -1
  32. package/dist/types.d.ts +5 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/utils.d.ts +2 -2
  35. package/dist/utils.d.ts.map +1 -1
  36. package/package.json +2 -1
  37. package/src/errors.js +33 -0
  38. package/src/invite/StateDiagram.md +47 -0
  39. package/src/invite/invite-api.js +387 -0
  40. package/src/invite/invite-state-machine.js +208 -0
  41. package/src/local-peers.js +12 -9
  42. package/src/mapeo-manager.js +1 -1
  43. package/src/member-api.js +5 -4
  44. package/src/types.ts +6 -0
  45. package/src/utils.js +8 -3
  46. 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
+ })
@@ -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
- #assertConnected() {
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
  }
@@ -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
- const inviteAbortedError = new Error('Invite aborted')
216
-
217
- if (signal.aborted) throw inviteAbortedError
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 inviteAbortedError
250
+ throw new InviteAbortedError()
250
251
  } else {
251
252
  throw err
252
253
  }
package/src/types.ts CHANGED
@@ -155,3 +155,9 @@ export type BlobStoreEntriesStream = Readable & {
155
155
  HyperdriveEntry & { driveId: string; blobCoreId: string }
156
156
  >
157
157
  }
158
+
159
+ export type StringToTaggedUnion<T extends string> = {
160
+ [K in T]: {
161
+ type: K
162
+ }
163
+ }[T]
package/src/utils.js CHANGED
@@ -34,11 +34,16 @@ export function noop() {}
34
34
 
35
35
  /**
36
36
  * @param {unknown} condition
37
- * @param {string} message
37
+ * @param {string | Error} messageOrError
38
38
  * @returns {asserts condition}
39
39
  */
40
- export function assert(condition, message) {
41
- if (!condition) throw new Error(message)
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
  /**