livekit-client 2.18.10 → 2.19.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 (59) hide show
  1. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  2. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  3. package/dist/livekit-client.esm.mjs +720 -430
  4. package/dist/livekit-client.esm.mjs.map +1 -1
  5. package/dist/livekit-client.pt.worker.js.map +1 -1
  6. package/dist/livekit-client.pt.worker.mjs.map +1 -1
  7. package/dist/livekit-client.umd.js +1 -1
  8. package/dist/livekit-client.umd.js.map +1 -1
  9. package/dist/src/room/RTCEngine.d.ts +0 -3
  10. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  11. package/dist/src/room/Room.d.ts +4 -2
  12. package/dist/src/room/Room.d.ts.map +1 -1
  13. package/dist/src/room/participant/LocalParticipant.d.ts +5 -13
  14. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  15. package/dist/src/room/participant/RemoteParticipant.d.ts +5 -1
  16. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  17. package/dist/src/room/rpc/client/RpcClientManager.d.ts +39 -0
  18. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -0
  19. package/dist/src/room/rpc/client/events.d.ts +8 -0
  20. package/dist/src/room/rpc/client/events.d.ts.map +1 -0
  21. package/dist/src/room/rpc/index.d.ts +6 -0
  22. package/dist/src/room/rpc/index.d.ts.map +1 -0
  23. package/dist/src/room/rpc/server/RpcServerManager.d.ts +44 -0
  24. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -0
  25. package/dist/src/room/rpc/server/events.d.ts +8 -0
  26. package/dist/src/room/rpc/server/events.d.ts.map +1 -0
  27. package/dist/src/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
  28. package/dist/src/room/rpc/utils.d.ts.map +1 -0
  29. package/dist/src/room/utils.d.ts.map +1 -1
  30. package/dist/src/version.d.ts +8 -0
  31. package/dist/src/version.d.ts.map +1 -1
  32. package/dist/ts4.2/room/RTCEngine.d.ts +0 -3
  33. package/dist/ts4.2/room/Room.d.ts +4 -2
  34. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +5 -13
  35. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +5 -1
  36. package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -0
  37. package/dist/ts4.2/room/rpc/client/events.d.ts +8 -0
  38. package/dist/ts4.2/room/rpc/index.d.ts +7 -0
  39. package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -0
  40. package/dist/ts4.2/room/rpc/server/events.d.ts +8 -0
  41. package/dist/ts4.2/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
  42. package/dist/ts4.2/version.d.ts +8 -0
  43. package/package.json +1 -1
  44. package/src/room/RTCEngine.ts +0 -26
  45. package/src/room/Room.ts +83 -81
  46. package/src/room/participant/LocalParticipant.ts +16 -180
  47. package/src/room/participant/RemoteParticipant.ts +9 -0
  48. package/src/room/rpc/client/RpcClientManager.test.ts +430 -0
  49. package/src/room/rpc/client/RpcClientManager.ts +269 -0
  50. package/src/room/rpc/client/events.ts +9 -0
  51. package/src/room/rpc/index.ts +14 -0
  52. package/src/room/rpc/server/RpcServerManager.test.ts +471 -0
  53. package/src/room/rpc/server/RpcServerManager.ts +293 -0
  54. package/src/room/rpc/server/events.ts +9 -0
  55. package/src/room/{rpc.ts → rpc/utils.ts} +49 -8
  56. package/src/room/utils.ts +2 -1
  57. package/src/version.ts +10 -0
  58. package/dist/src/room/rpc.d.ts.map +0 -1
  59. package/src/room/rpc.test.ts +0 -301
@@ -0,0 +1,293 @@
1
+ import { DataPacket, DataPacket_Kind, RpcAck, RpcRequest, RpcResponse } from '@livekit/protocol';
2
+ import EventEmitter from 'events';
3
+ import type TypedEmitter from 'typed-emitter';
4
+ import { type StructuredLogger } from '../../../logger';
5
+ import { CLIENT_PROTOCOL_DATA_STREAM_RPC } from '../../../version';
6
+ import { type TextStreamReader } from '../../data-stream/incoming/StreamReader';
7
+ import type OutgoingDataStreamManager from '../../data-stream/outgoing/OutgoingDataStreamManager';
8
+ import type Participant from '../../participant/Participant';
9
+ import {
10
+ MAX_V1_PAYLOAD_BYTES,
11
+ RPC_RESPONSE_DATA_STREAM_TOPIC,
12
+ RPC_VERSION_V2,
13
+ RpcError,
14
+ type RpcInvocationData,
15
+ RpcRequestAttrs,
16
+ byteLength,
17
+ } from '../utils';
18
+ import type { RpcServerManagerCallbacks } from './events';
19
+
20
+ /**
21
+ * Manages the server (handler) side of RPC: processing incoming requests,
22
+ * managing registered method handlers, and sending responses.
23
+ * @internal
24
+ */
25
+ export default class RpcServerManager extends (EventEmitter as new () => TypedEmitter<RpcServerManagerCallbacks>) {
26
+ private log: StructuredLogger;
27
+
28
+ private outgoingDataStreamManager: OutgoingDataStreamManager;
29
+
30
+ private getRemoteParticipantClientProtocol: (identity: Participant['identity']) => number;
31
+
32
+ private rpcHandlers: Map<string, (data: RpcInvocationData) => Promise<string>> = new Map();
33
+
34
+ constructor(
35
+ log: StructuredLogger,
36
+ outgoingDataStreamManager: OutgoingDataStreamManager,
37
+ getRemoteParticipantClientProtocol: (identity: Participant['identity']) => number,
38
+ ) {
39
+ super();
40
+ this.log = log;
41
+ this.outgoingDataStreamManager = outgoingDataStreamManager;
42
+ this.getRemoteParticipantClientProtocol = getRemoteParticipantClientProtocol;
43
+ }
44
+
45
+ registerRpcMethod(method: string, handler: (data: RpcInvocationData) => Promise<string>) {
46
+ if (this.rpcHandlers.has(method)) {
47
+ throw Error(
48
+ `RPC handler already registered for method ${method}, unregisterRpcMethod before trying to register again`,
49
+ );
50
+ }
51
+ this.rpcHandlers.set(method, handler);
52
+ }
53
+
54
+ unregisterRpcMethod(method: string) {
55
+ this.rpcHandlers.delete(method);
56
+ }
57
+
58
+ /**
59
+ * Handle an incoming RPCRequest message containing a payload.
60
+ * This handles "version 1" of rpc requests.
61
+ * @internal
62
+ */
63
+ async handleIncomingRpcRequest(callerIdentity: string, rpcRequest: RpcRequest) {
64
+ this.publishRpcAck(callerIdentity, rpcRequest.id);
65
+
66
+ if (rpcRequest.version !== 1) {
67
+ this.publishRpcResponsePacket(
68
+ callerIdentity,
69
+ rpcRequest.id,
70
+ null,
71
+ RpcError.builtIn('UNSUPPORTED_VERSION'),
72
+ );
73
+ return;
74
+ }
75
+
76
+ const handler = this.rpcHandlers.get(rpcRequest.method);
77
+
78
+ if (!handler) {
79
+ this.publishRpcResponsePacket(
80
+ callerIdentity,
81
+ rpcRequest.id,
82
+ null,
83
+ RpcError.builtIn('UNSUPPORTED_METHOD'),
84
+ );
85
+ return;
86
+ }
87
+
88
+ let response;
89
+ try {
90
+ response = await handler({
91
+ requestId: rpcRequest.id,
92
+ callerIdentity,
93
+ payload: rpcRequest.payload,
94
+ responseTimeout: rpcRequest.responseTimeoutMs,
95
+ });
96
+ } catch (error) {
97
+ let responseError;
98
+ if (error instanceof RpcError) {
99
+ responseError = error;
100
+ } else {
101
+ this.log.warn(
102
+ `Uncaught error returned by RPC handler for ${rpcRequest.method}. Returning APPLICATION_ERROR instead.`,
103
+ error,
104
+ );
105
+ responseError = RpcError.builtIn(
106
+ 'APPLICATION_ERROR',
107
+ `Uncaught error: ${(error as Error)?.message ?? error}`,
108
+ { cause: error },
109
+ );
110
+ }
111
+
112
+ this.publishRpcResponsePacket(callerIdentity, rpcRequest.id, null, responseError);
113
+ return;
114
+ }
115
+
116
+ await this.publishRpcResponse(callerIdentity, rpcRequest.id, response ?? '');
117
+ }
118
+
119
+ /**
120
+ * Handle an incoming data stream containing a RPC request payload.
121
+ * This handles "version 2" of rpc requests.
122
+ * @internal
123
+ */
124
+ async handleIncomingDataStream(
125
+ reader: TextStreamReader,
126
+ callerIdentity: Participant['identity'],
127
+ dataStreamAttrs: Record<string, string>,
128
+ ) {
129
+ const requestId = dataStreamAttrs[RpcRequestAttrs.RPC_REQUEST_ID];
130
+ const method = dataStreamAttrs[RpcRequestAttrs.RPC_REQUEST_METHOD];
131
+ const responseTimeout = parseInt(
132
+ dataStreamAttrs[RpcRequestAttrs.RPC_REQUEST_RESPONSE_TIMEOUT_MS],
133
+ 10,
134
+ );
135
+ const version = parseInt(dataStreamAttrs[RpcRequestAttrs.RPC_REQUEST_VERSION], 10);
136
+
137
+ if (!requestId || !method || Number.isNaN(responseTimeout) || Number.isNaN(version)) {
138
+ this.log.warn(
139
+ `RPC data stream malformed: ${RpcRequestAttrs.RPC_REQUEST_ID} / ${RpcRequestAttrs.RPC_REQUEST_METHOD} / ${RpcRequestAttrs.RPC_REQUEST_RESPONSE_TIMEOUT_MS} / ${RpcRequestAttrs.RPC_REQUEST_VERSION} not set.`,
140
+ );
141
+ this.publishRpcResponsePacket(
142
+ callerIdentity,
143
+ requestId,
144
+ null,
145
+ RpcError.builtIn('APPLICATION_ERROR', 'RPC data stream malformed'),
146
+ );
147
+ return;
148
+ }
149
+
150
+ this.publishRpcAck(callerIdentity, requestId);
151
+
152
+ if (version !== RPC_VERSION_V2) {
153
+ this.publishRpcResponsePacket(
154
+ callerIdentity,
155
+ requestId,
156
+ null,
157
+ RpcError.builtIn('UNSUPPORTED_VERSION'),
158
+ );
159
+ return;
160
+ }
161
+
162
+ let payload: string;
163
+ try {
164
+ payload = await reader.readAll();
165
+ } catch (e) {
166
+ this.log.warn(`Error reading RPC request payload: ${e}`);
167
+ this.publishRpcResponsePacket(
168
+ callerIdentity,
169
+ requestId,
170
+ null,
171
+ RpcError.builtIn('APPLICATION_ERROR', 'Error reading RPC request payload', { cause: e }),
172
+ );
173
+ return;
174
+ }
175
+
176
+ const handler = this.rpcHandlers.get(method);
177
+
178
+ if (!handler) {
179
+ this.publishRpcResponsePacket(
180
+ callerIdentity,
181
+ requestId,
182
+ null,
183
+ RpcError.builtIn('UNSUPPORTED_METHOD'),
184
+ );
185
+ return;
186
+ }
187
+
188
+ let response;
189
+ try {
190
+ response = await handler({
191
+ requestId,
192
+ callerIdentity,
193
+ payload,
194
+ responseTimeout,
195
+ });
196
+ } catch (error) {
197
+ let responseError;
198
+ if (error instanceof RpcError) {
199
+ responseError = error;
200
+ } else {
201
+ this.log.warn(
202
+ `Uncaught error returned by RPC handler for ${method}. Returning APPLICATION_ERROR instead.`,
203
+ error,
204
+ );
205
+ responseError = RpcError.builtIn('APPLICATION_ERROR');
206
+ }
207
+
208
+ this.publishRpcResponsePacket(callerIdentity, requestId, null, responseError);
209
+ return;
210
+ }
211
+
212
+ await this.publishRpcResponse(callerIdentity, requestId, response ?? '');
213
+ }
214
+
215
+ private publishRpcAck(destinationIdentity: string, requestId: string) {
216
+ this.emit('sendDataPacket', {
217
+ packet: new DataPacket({
218
+ destinationIdentities: [destinationIdentity],
219
+ kind: DataPacket_Kind.RELIABLE,
220
+ value: {
221
+ case: 'rpcAck',
222
+ value: new RpcAck({
223
+ requestId,
224
+ }),
225
+ },
226
+ }),
227
+ });
228
+ }
229
+
230
+ private publishRpcResponsePacket(
231
+ destinationIdentity: string,
232
+ requestId: string,
233
+ payload: string | null,
234
+ error: RpcError | null,
235
+ ) {
236
+ this.emit('sendDataPacket', {
237
+ packet: new DataPacket({
238
+ destinationIdentities: [destinationIdentity],
239
+ kind: DataPacket_Kind.RELIABLE,
240
+ value: {
241
+ case: 'rpcResponse',
242
+ value: new RpcResponse({
243
+ requestId,
244
+ value: error
245
+ ? { case: 'error', value: error.toProto() }
246
+ : { case: 'payload', value: payload ?? '' },
247
+ }),
248
+ },
249
+ }),
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Send a successful RPC response payload, choosing the transport based on
255
+ * the caller's client protocol version.
256
+ */
257
+ private async publishRpcResponse(
258
+ destinationIdentity: string,
259
+ requestId: string,
260
+ payload: string,
261
+ ) {
262
+ const callerClientProtocol = this.getRemoteParticipantClientProtocol(destinationIdentity);
263
+
264
+ if (callerClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
265
+ // Send response as a data stream
266
+ const writer = await this.outgoingDataStreamManager.streamText({
267
+ topic: RPC_RESPONSE_DATA_STREAM_TOPIC,
268
+ destinationIdentities: [destinationIdentity],
269
+ attributes: { [RpcRequestAttrs.RPC_REQUEST_ID]: requestId },
270
+ });
271
+ await writer.write(payload);
272
+ await writer.close();
273
+ return;
274
+ }
275
+
276
+ // Legacy client: enforce size limit and send uncompressed payload inline
277
+ const responseBytes = byteLength(payload);
278
+ if (responseBytes > MAX_V1_PAYLOAD_BYTES) {
279
+ this.log.warn(
280
+ `RPC Response payload too large for request ${requestId}. To send larger responses, consider updating the sending client.`,
281
+ );
282
+ this.publishRpcResponsePacket(
283
+ destinationIdentity,
284
+ requestId,
285
+ null,
286
+ RpcError.builtIn('RESPONSE_PAYLOAD_TOO_LARGE'),
287
+ );
288
+ return;
289
+ }
290
+
291
+ this.publishRpcResponsePacket(destinationIdentity, requestId, payload, null);
292
+ }
293
+ }
@@ -0,0 +1,9 @@
1
+ import type { DataPacket } from '@livekit/protocol';
2
+
3
+ export type EventSendDataPacket = {
4
+ packet: DataPacket;
5
+ };
6
+
7
+ export type RpcServerManagerCallbacks = {
8
+ sendDataPacket: (event: EventSendDataPacket) => void;
9
+ };
@@ -1,6 +1,3 @@
1
- // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
- //
3
- // SPDX-License-Identifier: Apache-2.0
4
1
  import { RpcError as RpcError_Proto } from '@livekit/protocol';
5
2
 
6
3
  /** Parameters for initiating an RPC call */
@@ -63,6 +60,9 @@ export class RpcError extends Error {
63
60
 
64
61
  data?: string;
65
62
 
63
+ // More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
64
+ cause?: unknown;
65
+
66
66
  /**
67
67
  * Creates an error object with the given code and message, plus an optional data payload.
68
68
  *
@@ -70,11 +70,15 @@ export class RpcError extends Error {
70
70
  *
71
71
  * Error codes 1001-1999 are reserved for built-in errors (see RpcError.ErrorCode for their meanings).
72
72
  */
73
- constructor(code: number, message: string, data?: string) {
73
+ constructor(code: number, message: string, data?: string, options?: { cause?: unknown }) {
74
74
  super(message);
75
75
  this.code = code;
76
76
  this.message = truncateBytes(message, RpcError.MAX_MESSAGE_BYTES);
77
77
  this.data = data ? truncateBytes(data, RpcError.MAX_DATA_BYTES) : undefined;
78
+
79
+ if (typeof options?.cause !== 'undefined') {
80
+ this.cause = options?.cause;
81
+ }
78
82
  }
79
83
 
80
84
  /**
@@ -133,16 +137,53 @@ export class RpcError extends Error {
133
137
  *
134
138
  * @internal
135
139
  */
136
- static builtIn(key: keyof typeof RpcError.ErrorCode, data?: string): RpcError {
137
- return new RpcError(RpcError.ErrorCode[key], RpcError.ErrorMessage[key], data);
140
+ static builtIn(
141
+ key: keyof typeof RpcError.ErrorCode,
142
+ data?: string,
143
+ options?: { cause?: unknown },
144
+ ): RpcError {
145
+ return new RpcError(RpcError.ErrorCode[key], RpcError.ErrorMessage[key], data, options);
138
146
  }
139
147
  }
140
148
 
141
149
  /*
142
- * Maximum payload size for RPC requests and responses. If a payload exceeds this size,
150
+ * Maximum payload size for RPC requests and responses for clients with a clientProtocol of less
151
+ * than CLIENT_PROTOCOL_DATA_STREAM_RPC.
152
+ *
153
+ * If a payload exceeds this size and the remote client does not support compression,
143
154
  * the RPC call will fail with a REQUEST_PAYLOAD_TOO_LARGE(1402) or RESPONSE_PAYLOAD_TOO_LARGE(1504) error.
144
155
  */
145
- export const MAX_PAYLOAD_BYTES = 15360; // 15 KB
156
+ export const MAX_V1_PAYLOAD_BYTES = 15360; // 15 KB
157
+
158
+ /**
159
+ * Topic used for v2 RPC request data streams.
160
+ * @internal
161
+ */
162
+ export const RPC_REQUEST_DATA_STREAM_TOPIC = 'lk.rpc_request';
163
+
164
+ /**
165
+ * Topic used for v2 RPC response data streams.
166
+ * @internal
167
+ */
168
+ export const RPC_RESPONSE_DATA_STREAM_TOPIC = 'lk.rpc_response';
169
+
170
+ /** @internal */
171
+ export enum RpcRequestAttrs {
172
+ RPC_REQUEST_ID = 'lk.rpc_request_id',
173
+ RPC_REQUEST_METHOD = 'lk.rpc_request_method',
174
+ RPC_REQUEST_RESPONSE_TIMEOUT_MS = 'lk.rpc_request_response_timeout_ms',
175
+ RPC_REQUEST_VERSION = 'lk.rpc_request_version',
176
+ }
177
+
178
+ /** Initial version of rpc which uses RpcRequest / RpcResponse messages.
179
+ * @internal
180
+ **/
181
+ export const RPC_VERSION_V1 = 1;
182
+
183
+ /** Rpc version backed by data streams instead of RpcRequest / RpcResponse.
184
+ * @internal
185
+ **/
186
+ export const RPC_VERSION_V2 = 2;
146
187
 
147
188
  /**
148
189
  * @internal
package/src/room/utils.ts CHANGED
@@ -10,7 +10,7 @@ import { type Throws } from '@livekit/throws-transformer/throws';
10
10
  import TypedPromise from '../utils/TypedPromise';
11
11
  import { getBrowser } from '../utils/browserParser';
12
12
  import type { BrowserDetails } from '../utils/browserParser';
13
- import { protocolVersion, version } from '../version';
13
+ import { clientProtocol, protocolVersion, version } from '../version';
14
14
  import { type ConnectionError, ConnectionErrorReason } from './errors';
15
15
  import type LocalParticipant from './participant/LocalParticipant';
16
16
  import type Participant from './participant/Participant';
@@ -382,6 +382,7 @@ export function getClientInfo(capabilities?: ClientInfo_Capability[]): ClientInf
382
382
  capabilities,
383
383
  sdk: ClientInfo_SDK.JS,
384
384
  protocol: protocolVersion,
385
+ clientProtocol,
385
386
  version,
386
387
  });
387
388
 
package/src/version.ts CHANGED
@@ -2,3 +2,13 @@ import { version as v } from '../package.json';
2
2
 
3
3
  export const version = v;
4
4
  export const protocolVersion = 17;
5
+
6
+ /** Initial client protocol. */
7
+ export const CLIENT_PROTOCOL_DEFAULT = 0;
8
+ /** Replaces RPC v1 protocol with a v2 data streams based one to support unlimited request /
9
+ * response payload length. */
10
+ export const CLIENT_PROTOCOL_DATA_STREAM_RPC = 1;
11
+
12
+ /** The client protocol version indicates what level of support that the client has for
13
+ * client <-> client api interactions. */
14
+ export const clientProtocol = CLIENT_PROTOCOL_DATA_STREAM_RPC;
@@ -1 +0,0 @@
1
- {"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../../../src/room/rpc.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,IAAI,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE/D,4CAA4C;AAC5C,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AAEH,qBAAa,QAAS,SAAQ,KAAK;IACjC,MAAM,CAAC,iBAAiB,SAAO;IAE/B,MAAM,CAAC,cAAc,SAAS;IAE9B,IAAI,EAAE,MAAM,CAAC;IAEb,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;;;OAMG;gBACS,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;IAOxD;;OAEG;IACH,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,cAAc;IAItC;;OAEG;IACH,OAAO;IAQP,MAAM,CAAC,SAAS;;;;;;;;;;;;MAaL;IAEX;;OAEG;IACH,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,OAAO,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,CAazD;IAEX;;;;OAIG;IACH,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,MAAM,OAAO,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ;CAG9E;AAMD,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG9C;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAmBnE"}