@comapeo/core 2.3.2 → 3.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.
@@ -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
+ })
@@ -64,6 +64,7 @@ class Peer {
64
64
  #deviceType
65
65
  #connectedAt = 0
66
66
  #disconnectedAt = 0
67
+ #drainedListeners = new Set()
67
68
  #protomux
68
69
  #log
69
70
 
@@ -157,53 +158,94 @@ class Peer {
157
158
  this.#disconnectedAt = Date.now()
158
159
  // This promise should have already resolved, but if the peer never connected then we reject here
159
160
  this.#connected.reject(new PeerFailedConnectionError())
161
+ for (const listener of this.#drainedListeners) {
162
+ listener.reject(new Error('RPC Disconnected before sending'))
163
+ }
164
+ this.#drainedListeners.clear()
160
165
  this.#log('disconnected')
161
166
  }
167
+
168
+ // Call this when the stream has drained all data to the network
169
+ drained() {
170
+ for (const listener of this.#drainedListeners) {
171
+ listener.resolve()
172
+ }
173
+ this.#drainedListeners.clear()
174
+ }
175
+
176
+ /**
177
+ * @param {boolean} didWrite
178
+ * @returns {Promise<void>}
179
+ */
180
+ async #waitForDrain(didWrite) {
181
+ if (didWrite) return
182
+ const onDrain = pDefer()
183
+
184
+ this.#drainedListeners.add(onDrain)
185
+
186
+ await onDrain.promise
187
+ }
188
+
162
189
  /**
163
190
  * @param {Buffer} buf
191
+ * @returns {Promise<void>}
164
192
  */
165
- [kTestOnlySendRawInvite](buf) {
193
+ async [kTestOnlySendRawInvite](buf) {
166
194
  this.#assertConnected()
167
195
  const messageType = MESSAGE_TYPES.Invite
168
- this.#channel.messages[messageType].send(buf)
196
+ await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
169
197
  }
170
- /** @param {Invite} invite */
171
- sendInvite(invite) {
172
- this.#assertConnected()
198
+ /**
199
+ * @param {Invite} invite
200
+ * @returns {Promise<void>}
201
+ */
202
+ async sendInvite(invite) {
203
+ this.#assertConnected('Peer disconnected before sending invite')
173
204
  const buf = Buffer.from(Invite.encode(invite).finish())
174
205
  const messageType = MESSAGE_TYPES.Invite
175
- this.#channel.messages[messageType].send(buf)
206
+ await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
176
207
  this.#log('sent invite %h', invite.inviteId)
177
208
  }
178
- /** @param {InviteCancel} inviteCancel */
179
- sendInviteCancel(inviteCancel) {
180
- this.#assertConnected()
209
+ /**
210
+ * @param {InviteCancel} inviteCancel
211
+ * @returns {Promise<void>}
212
+ */
213
+ async sendInviteCancel(inviteCancel) {
214
+ this.#assertConnected('Peer disconnected before sending invite cancel')
181
215
  const buf = Buffer.from(InviteCancel.encode(inviteCancel).finish())
182
216
  const messageType = MESSAGE_TYPES.InviteCancel
183
- this.#channel.messages[messageType].send(buf)
217
+ await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
184
218
  this.#log('sent invite cancel %h', inviteCancel.inviteId)
185
219
  }
186
- /** @param {InviteResponse} response */
187
- sendInviteResponse(response) {
188
- this.#assertConnected()
220
+ /**
221
+ * @param {InviteResponse} response
222
+ * @returns {Promise<void>}
223
+ */
224
+ async sendInviteResponse(response) {
225
+ this.#assertConnected('Peer disconnected before sending invite response')
189
226
  const buf = Buffer.from(InviteResponse.encode(response).finish())
190
227
  const messageType = MESSAGE_TYPES.InviteResponse
191
- this.#channel.messages[messageType].send(buf)
228
+ await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
192
229
  this.#log('sent response for %h: %s', response.inviteId, response.decision)
193
230
  }
194
231
  /** @param {ProjectJoinDetails} details */
195
- sendProjectJoinDetails(details) {
196
- this.#assertConnected()
232
+ async sendProjectJoinDetails(details) {
233
+ this.#assertConnected(
234
+ 'Peer disconnected before sending project join details'
235
+ )
197
236
  const buf = Buffer.from(ProjectJoinDetails.encode(details).finish())
198
237
  const messageType = MESSAGE_TYPES.ProjectJoinDetails
199
- this.#channel.messages[messageType].send(buf)
238
+ await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
200
239
  this.#log('sent project join details for %h', details.projectKey)
201
240
  }
202
- /** @param {DeviceInfo} deviceInfo */
203
- sendDeviceInfo(deviceInfo) {
241
+ /**
242
+ * @param {DeviceInfo} deviceInfo
243
+ * @returns {Promise<void>}
244
+ */
245
+ async sendDeviceInfo(deviceInfo) {
204
246
  const buf = Buffer.from(DeviceInfo.encode(deviceInfo).finish())
205
247
  const messageType = MESSAGE_TYPES.DeviceInfo
206
- this.#channel.messages[messageType].send(buf)
248
+ await this.#waitForDrain(this.#channel.messages[messageType].send(buf))
207
249
  this.#log('sent deviceInfo %o', deviceInfo)
208
250
  }
209
251
  /** @param {DeviceInfo} deviceInfo */
@@ -212,10 +254,11 @@ class Peer {
212
254
  this.#deviceType = deviceInfo.deviceType
213
255
  this.#log('received deviceInfo %o', deviceInfo)
214
256
  }
215
- #assertConnected() {
257
+ /** @param {string} [message] */
258
+ #assertConnected(message) {
216
259
  if (this.#state === 'connected' && !this.#channel.closed) return
217
260
  /* c8 ignore next */
218
- throw new PeerDisconnectedError() // TODO: report error - this should not happen
261
+ throw new PeerDisconnectedError(message) // TODO: report error - this should not happen
219
262
  }
220
263
  }
221
264
 
@@ -270,7 +313,7 @@ export class LocalPeers extends TypedEmitter {
270
313
  async sendInvite(deviceId, invite) {
271
314
  await this.#waitForPendingConnections()
272
315
  const peer = await this.#getPeerByDeviceId(deviceId)
273
- peer.sendInvite(invite)
316
+ await peer.sendInvite(invite)
274
317
  }
275
318
 
276
319
  /**
@@ -281,7 +324,7 @@ export class LocalPeers extends TypedEmitter {
281
324
  async sendInviteCancel(deviceId, inviteCancel) {
282
325
  await this.#waitForPendingConnections()
283
326
  const peer = await this.#getPeerByDeviceId(deviceId)
284
- peer.sendInviteCancel(inviteCancel)
327
+ await peer.sendInviteCancel(inviteCancel)
285
328
  }
286
329
 
287
330
  /**
@@ -293,7 +336,7 @@ export class LocalPeers extends TypedEmitter {
293
336
  async sendInviteResponse(deviceId, inviteResponse) {
294
337
  await this.#waitForPendingConnections()
295
338
  const peer = await this.#getPeerByDeviceId(deviceId)
296
- peer.sendInviteResponse(inviteResponse)
339
+ await peer.sendInviteResponse(inviteResponse)
297
340
  }
298
341
 
299
342
  /**
@@ -303,7 +346,7 @@ export class LocalPeers extends TypedEmitter {
303
346
  async sendProjectJoinDetails(deviceId, details) {
304
347
  await this.#waitForPendingConnections()
305
348
  const peer = await this.#getPeerByDeviceId(deviceId)
306
- peer.sendProjectJoinDetails(details)
349
+ await peer.sendProjectJoinDetails(details)
307
350
  }
308
351
 
309
352
  /**
@@ -314,7 +357,7 @@ export class LocalPeers extends TypedEmitter {
314
357
  async sendDeviceInfo(deviceId, deviceInfo) {
315
358
  await this.#waitForPendingConnections()
316
359
  const peer = await this.#getPeerByDeviceId(deviceId)
317
- peer.sendDeviceInfo(deviceInfo)
360
+ await peer.sendDeviceInfo(deviceInfo)
318
361
  }
319
362
 
320
363
  /**
@@ -446,6 +489,9 @@ export class LocalPeers extends TypedEmitter {
446
489
  this.#emitPeers()
447
490
  done()
448
491
  },
492
+ ondrain: () => {
493
+ peer.drained()
494
+ },
449
495
  })
450
496
  channel.open()
451
497
 
@@ -614,7 +660,7 @@ export { TimeoutError }
614
660
 
615
661
  export class UnknownPeerError extends Error {
616
662
  /** @param {string} [message] */
617
- constructor(message) {
663
+ constructor(message = 'UnknownPeerError') {
618
664
  super(message)
619
665
  this.name = 'UnknownPeerError'
620
666
  }
@@ -622,7 +668,7 @@ export class UnknownPeerError extends Error {
622
668
 
623
669
  export class PeerDisconnectedError extends Error {
624
670
  /** @param {string} [message] */
625
- constructor(message) {
671
+ constructor(message = 'Peer disconnected') {
626
672
  super(message)
627
673
  this.name = 'PeerDisconnectedError'
628
674
  }
@@ -630,7 +676,7 @@ export class PeerDisconnectedError extends Error {
630
676
 
631
677
  export class PeerFailedConnectionError extends Error {
632
678
  /** @param {string} [message] */
633
- constructor(message) {
679
+ constructor(message = 'PeerFailedConnectionError') {
634
680
  super(message)
635
681
  this.name = 'PeerFailedConnectionError'
636
682
  }
@@ -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
  /**
@@ -1,70 +0,0 @@
1
- /**
2
- * @typedef {Object} InviteApiEvents
3
- * @property {(invite: Invite) => void} invite-received
4
- * @property {(invite: Invite, removalReason: InviteRemovalReason) => void} invite-removed
5
- */
6
- /**
7
- * @extends {TypedEmitter<InviteApiEvents>}
8
- */
9
- export class InviteApi extends TypedEmitter<InviteApiEvents> {
10
- /**
11
- * @param {Object} options
12
- * @param {import('./local-peers.js').LocalPeers} options.rpc
13
- * @param {object} options.queries
14
- * @param {(projectInviteId: Readonly<Buffer>) => undefined | { projectPublicId: string }} options.queries.getProjectByInviteId
15
- * @param {(projectDetails: Pick<ProjectJoinDetails, 'projectKey' | 'encryptionKeys'> & { projectName: string }) => Promise<string>} options.queries.addProject
16
- * @param {Logger} [options.logger]
17
- */
18
- constructor({ rpc, queries, logger }: {
19
- rpc: import("./local-peers.js").LocalPeers;
20
- queries: {
21
- getProjectByInviteId: (projectInviteId: Readonly<Buffer>) => undefined | {
22
- projectPublicId: string;
23
- };
24
- addProject: (projectDetails: Pick<ProjectJoinDetails, "projectKey" | "encryptionKeys"> & {
25
- projectName: string;
26
- }) => Promise<string>;
27
- };
28
- logger?: Logger | undefined;
29
- });
30
- rpc: import("./local-peers.js").LocalPeers;
31
- /**
32
- * @returns {Array<Invite>}
33
- */
34
- getPending(): Array<Invite>;
35
- /**
36
- * Attempt to accept the invite.
37
- *
38
- * This can fail if the invitor has canceled the invite or if you cannot
39
- * connect to the invitor's device.
40
- *
41
- * If the invite is accepted and you had other invites to the same project,
42
- * those invites are removed, and the invitors are told that you're already
43
- * part of this project.
44
- *
45
- * @param {Pick<Invite, 'inviteId'>} invite
46
- * @returns {Promise<string>}
47
- */
48
- accept({ inviteId: inviteIdString }: Pick<Invite, "inviteId">): Promise<string>;
49
- /**
50
- * @param {Pick<Invite, 'inviteId'>} invite
51
- * @returns {void}
52
- */
53
- reject({ inviteId: inviteIdString }: Pick<Invite, "inviteId">): void;
54
- #private;
55
- }
56
- export type InviteInternal = InviteRpcMessage & {
57
- receivedAt: number;
58
- };
59
- export type Invite = MapBuffers<InviteInternal>;
60
- export type InviteRemovalReason = ("accepted" | "rejected" | "canceled" | "accepted other" | "connection error" | "internal error");
61
- export type InviteApiEvents = {
62
- "invite-received": (invite: Invite) => void;
63
- "invite-removed": (invite: Invite, removalReason: InviteRemovalReason) => void;
64
- };
65
- import { TypedEmitter } from 'tiny-typed-emitter';
66
- import type { ProjectJoinDetails } from './generated/rpc.js';
67
- import { Logger } from './logger.js';
68
- import type { Invite as InviteRpcMessage } from './generated/rpc.js';
69
- import type { MapBuffers } from './types.js';
70
- //# sourceMappingURL=invite-api.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"invite-api.d.ts","sourceRoot":"","sources":["../src/invite-api.js"],"names":[],"mappings":"AAiJA;;;;GAIG;AAEH;;GAEG;AACH;IAME;;;;;;;OAOG;IACH,sCANG;QAAuD,GAAG,EAAlD,OAAO,kBAAkB,EAAE,UAAU;QACrB,OAAO,EAC/B;YAAwG,oBAAoB,EAApH,CAAC,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,SAAS,GAAG;gBAAE,eAAe,EAAE,MAAM,CAAA;aAAE;YAC4D,UAAU,EAApJ,CAAC,cAAc,EAAE,IAAI,CAAC,kBAAkB,EAAE,YAAY,GAAG,gBAAgB,CAAC,GAAG;gBAAE,WAAW,EAAE,MAAM,CAAA;aAAE,KAAK,OAAO,CAAC,MAAM,CAAC;SAChI;QAAyB,MAAM;KAAC,EA0BlC;IAnBC,2CAAc;IAsFhB;;OAEG;IACH,cAFa,KAAK,CAAC,MAAM,CAAC,CAMzB;IAED;;;;;;;;;;;;OAYG;IACH,qCAHW,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,GACtB,OAAO,CAAC,MAAM,CAAC,CAkI3B;IAED;;;OAGG;IACH,qCAHW,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,GACtB,IAAI,CAuBhB;;CACF;6BA7ZY,gBAAgB,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE;qBAGxC,WAAW,cAAc,CAAC;kCAG3B,CACZ,UAAa,GACb,UAAa,GACb,UAAa,GACb,gBAAmB,GACnB,kBAAqB,GACrB,gBAAmB,CAChB;;uBA8GU,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI;sBACxB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,mBAAmB,KAAK,IAAI;;6BApJ7C,oBAAoB;wCAavC,oBAAoB;uBAPP,aAAa;gDAO1B,oBAAoB;gCANE,YAAY"}