yoto-nodejs-client 0.0.5 → 0.0.7
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/README.md +73 -40
- package/bin/auth.js +4 -3
- package/bin/device-model.js +25 -5
- package/bin/device-tui.js +25 -3
- package/bin/devices.js +25 -9
- package/bin/lib/cli-helpers.d.ts.map +1 -1
- package/bin/lib/cli-helpers.js +3 -1
- package/bin/lib/token-helpers.d.ts +4 -2
- package/bin/lib/token-helpers.d.ts.map +1 -1
- package/bin/lib/token-helpers.js +9 -8
- package/bin/refresh-token.js +4 -2
- package/bin/token-info.js +2 -2
- package/lib/api-client.d.ts +11 -10
- package/lib/api-client.d.ts.map +1 -1
- package/lib/api-client.js +12 -14
- package/lib/api-endpoints/auth.test.js +4 -4
- package/lib/api-endpoints/content.test.js +32 -32
- package/lib/api-endpoints/devices.d.ts +4 -4
- package/lib/api-endpoints/devices.js +2 -2
- package/lib/api-endpoints/devices.test.js +45 -45
- package/lib/api-endpoints/endpoint-test-helpers.d.ts +3 -4
- package/lib/api-endpoints/endpoint-test-helpers.d.ts.map +1 -1
- package/lib/api-endpoints/endpoint-test-helpers.js +21 -5
- package/lib/api-endpoints/family-library-groups.d.ts +3 -3
- package/lib/api-endpoints/family-library-groups.d.ts.map +1 -1
- package/lib/api-endpoints/family-library-groups.js +3 -3
- package/lib/api-endpoints/family-library-groups.test.js +29 -29
- package/lib/api-endpoints/family.test.js +11 -11
- package/lib/api-endpoints/icons.test.js +14 -14
- package/lib/mqtt/client.d.ts +123 -48
- package/lib/mqtt/client.d.ts.map +1 -1
- package/lib/mqtt/client.js +131 -49
- package/lib/mqtt/factory.d.ts +12 -5
- package/lib/mqtt/factory.d.ts.map +1 -1
- package/lib/mqtt/factory.js +39 -11
- package/lib/mqtt/index.js +2 -1
- package/lib/mqtt/mqtt.test.js +25 -22
- package/lib/test-helpers/device-model-test-helpers.d.ts +29 -0
- package/lib/test-helpers/device-model-test-helpers.d.ts.map +1 -0
- package/lib/test-helpers/device-model-test-helpers.js +116 -0
- package/lib/token.d.ts +44 -2
- package/lib/token.d.ts.map +1 -1
- package/lib/token.js +142 -2
- package/lib/yoto-account.d.ts +339 -9
- package/lib/yoto-account.d.ts.map +1 -1
- package/lib/yoto-account.js +411 -39
- package/lib/yoto-account.test.js +139 -0
- package/lib/yoto-device.d.ts +418 -30
- package/lib/yoto-device.d.ts.map +1 -1
- package/lib/yoto-device.js +670 -104
- package/lib/yoto-device.test.js +88 -0
- package/package.json +1 -1
package/lib/token.js
CHANGED
|
@@ -2,14 +2,26 @@ import { EventEmitter } from 'node:events'
|
|
|
2
2
|
import { jwtDecode } from 'jwt-decode'
|
|
3
3
|
import { exchangeToken } from './api-endpoints/auth.js'
|
|
4
4
|
|
|
5
|
+
const AUTO_REFRESH_RETRY_BASE_SECONDS = 5
|
|
6
|
+
const AUTO_REFRESH_RETRY_MAX_SECONDS = 300
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* @typedef {Object} RefreshableTokenOpts
|
|
7
10
|
* @property {string} clientId - OAuth client ID
|
|
8
11
|
* @property {string} refreshToken - OAuth refresh token
|
|
9
12
|
* @property {string} accessToken - Initial OAuth access token (JWT)
|
|
13
|
+
* @property {OnTokenRefreshHandler} onTokenRefresh - A function that will receive the refreshed token info and perisist it for future use
|
|
10
14
|
* @property {number} [bufferSeconds=30] - Seconds before expiration to consider token expired
|
|
11
15
|
*/
|
|
12
16
|
|
|
17
|
+
/**
|
|
18
|
+
* This is a REQUIRED function. It gets called after the token is refreshed with new tokens.
|
|
19
|
+
* It should save the new tokens somewhere. When starting back up, re-use the newly perissted tokens.
|
|
20
|
+
* If you loose track of these new tokens, you will effectively be logged out.
|
|
21
|
+
*
|
|
22
|
+
* @typedef {(refreshSuccessEvent: RefreshSuccessEvent) => void | Promise<void>} OnTokenRefreshHandler
|
|
23
|
+
*/
|
|
24
|
+
|
|
13
25
|
/**
|
|
14
26
|
* @typedef {Object} RefreshSuccessEvent
|
|
15
27
|
* @property {string} clientId - The OAuth client ID
|
|
@@ -37,12 +49,13 @@ import { exchangeToken } from './api-endpoints/auth.js'
|
|
|
37
49
|
*
|
|
38
50
|
* Events:
|
|
39
51
|
* - 'refresh:start' - Emitted when token refresh begins
|
|
40
|
-
* - 'refresh:success' - Emitted when token refresh succeeds, passes { clientId,
|
|
52
|
+
* - 'refresh:success' - Emitted when token refresh succeeds, passes { clientId, updatedAccessToken, updatedRefreshToken, updatedExpiresAt, prevAccessToken, prevRefreshToken, prevExpiresAt }
|
|
41
53
|
* - 'refresh:error' - Emitted when token refresh fails (transient errors), passes error
|
|
42
54
|
* - 'invalid' - Emitted when refresh token is permanently invalid, passes error
|
|
43
55
|
*
|
|
44
56
|
* @extends {EventEmitter<RefreshableTokenEventMap>}
|
|
45
57
|
*/
|
|
58
|
+
|
|
46
59
|
export class RefreshableToken extends EventEmitter {
|
|
47
60
|
/** @type {string} */
|
|
48
61
|
#clientId
|
|
@@ -58,16 +71,25 @@ export class RefreshableToken extends EventEmitter {
|
|
|
58
71
|
#invalid = false
|
|
59
72
|
/** @type {Promise<RefreshSuccessEvent> | null} */
|
|
60
73
|
#inFlightRefresh = null
|
|
74
|
+
/** @type {boolean} */
|
|
75
|
+
#autoRefreshEnabled = true
|
|
76
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
77
|
+
#autoRefreshTimeout = null
|
|
78
|
+
/** @type {number} */
|
|
79
|
+
#autoRefreshRetryAttempt = 0
|
|
80
|
+
/** @type {OnTokenRefreshHandler} */
|
|
81
|
+
#onTokenRefresh
|
|
61
82
|
|
|
62
83
|
/**
|
|
63
84
|
* @param {RefreshableTokenOpts} opts
|
|
64
85
|
*/
|
|
65
|
-
constructor ({ clientId, refreshToken, accessToken, bufferSeconds = 30 }) {
|
|
86
|
+
constructor ({ clientId, refreshToken, accessToken, bufferSeconds = 30, onTokenRefresh }) {
|
|
66
87
|
super()
|
|
67
88
|
this.#clientId = clientId
|
|
68
89
|
this.#refreshToken = refreshToken
|
|
69
90
|
this.#accessToken = accessToken
|
|
70
91
|
this.#bufferSeconds = bufferSeconds
|
|
92
|
+
this.#onTokenRefresh = onTokenRefresh
|
|
71
93
|
|
|
72
94
|
// Decode the JWT to get expiration
|
|
73
95
|
try {
|
|
@@ -80,6 +102,8 @@ export class RefreshableToken extends EventEmitter {
|
|
|
80
102
|
const error = /** @type {Error} */ (err)
|
|
81
103
|
throw new Error(`Failed to decode access token: ${error.message}`)
|
|
82
104
|
}
|
|
105
|
+
|
|
106
|
+
this.#scheduleAutoRefresh()
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
/**
|
|
@@ -170,8 +194,13 @@ export class RefreshableToken extends EventEmitter {
|
|
|
170
194
|
prevExpiresAt,
|
|
171
195
|
}
|
|
172
196
|
|
|
197
|
+
await this.#onTokenRefresh(eventPayload)
|
|
198
|
+
|
|
173
199
|
this.emit('refresh:success', eventPayload)
|
|
174
200
|
|
|
201
|
+
this.#autoRefreshRetryAttempt = 0
|
|
202
|
+
this.#scheduleAutoRefresh()
|
|
203
|
+
|
|
175
204
|
return eventPayload
|
|
176
205
|
} catch (err) {
|
|
177
206
|
const error = /** @type {any} */ (err)
|
|
@@ -188,6 +217,7 @@ export class RefreshableToken extends EventEmitter {
|
|
|
188
217
|
if (error.body?.error && invalidRefreshErrors.includes(error.body.error)) {
|
|
189
218
|
// Mark this token as permanently invalid
|
|
190
219
|
this.#invalid = true
|
|
220
|
+
this.#clearAutoRefreshTimeout()
|
|
191
221
|
const statusCode = error.statusCode ? ` (${error.statusCode})` : ''
|
|
192
222
|
const invalidError = new Error(`Refresh token is invalid or expired${statusCode}: ${error.body.error}${error.body.error_description ? ` - ${error.body.error_description}` : ''}`)
|
|
193
223
|
this.emit('invalid', invalidError)
|
|
@@ -196,10 +226,120 @@ export class RefreshableToken extends EventEmitter {
|
|
|
196
226
|
|
|
197
227
|
// For other errors, rethrow without marking as invalid (might be transient)
|
|
198
228
|
this.emit('refresh:error', error)
|
|
229
|
+
this.#scheduleAutoRefreshRetry()
|
|
199
230
|
throw error
|
|
200
231
|
}
|
|
201
232
|
}
|
|
202
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Stop background token refresh scheduling.
|
|
236
|
+
*/
|
|
237
|
+
stopAutoRefresh () {
|
|
238
|
+
this.#autoRefreshEnabled = false
|
|
239
|
+
this.#clearAutoRefreshTimeout()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Start background token refresh scheduling (enabled by default).
|
|
244
|
+
*/
|
|
245
|
+
startAutoRefresh () {
|
|
246
|
+
if (this.#invalid) {
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.#autoRefreshEnabled = true
|
|
251
|
+
this.#autoRefreshRetryAttempt = 0
|
|
252
|
+
this.#scheduleAutoRefresh()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check if background token refresh scheduling is enabled.
|
|
257
|
+
* @returns {boolean}
|
|
258
|
+
*/
|
|
259
|
+
isAutoRefreshEnabled () {
|
|
260
|
+
return this.#autoRefreshEnabled
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get the current OAuth client ID (synchronous).
|
|
265
|
+
* @returns {string}
|
|
266
|
+
*/
|
|
267
|
+
get clientId () {
|
|
268
|
+
return this.#clientId
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get the latest access token value (synchronous).
|
|
273
|
+
* Prefer getAccessToken() if you need a guaranteed-valid token in async contexts.
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
get accessToken () {
|
|
277
|
+
return this.#accessToken
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Schedule the next refresh for when the token becomes stale (expiresAt - bufferSeconds).
|
|
282
|
+
* Uses an unref'd timer so it doesn't keep the process alive.
|
|
283
|
+
*/
|
|
284
|
+
#scheduleAutoRefresh () {
|
|
285
|
+
if (!this.#autoRefreshEnabled || this.#invalid) {
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const now = Math.floor(Date.now() / 1000)
|
|
290
|
+
const refreshAt = this.#expiresAt - this.#bufferSeconds
|
|
291
|
+
const delayMs = Math.max(0, (refreshAt - now) * 1000)
|
|
292
|
+
|
|
293
|
+
this.#clearAutoRefreshTimeout()
|
|
294
|
+
this.#autoRefreshTimeout = setTimeout(() => {
|
|
295
|
+
this.#autoRefreshTimeout = null
|
|
296
|
+
if (this.#invalid) {
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.#refreshAccessToken().catch(() => {
|
|
301
|
+
// Errors are emitted by #performRefresh; retries are scheduled there as well.
|
|
302
|
+
})
|
|
303
|
+
}, delayMs)
|
|
304
|
+
|
|
305
|
+
this.#autoRefreshTimeout.unref?.()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#clearAutoRefreshTimeout () {
|
|
309
|
+
if (!this.#autoRefreshTimeout) {
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
clearTimeout(this.#autoRefreshTimeout)
|
|
314
|
+
this.#autoRefreshTimeout = null
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#scheduleAutoRefreshRetry () {
|
|
318
|
+
if (!this.#autoRefreshEnabled || this.#invalid) {
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this.#autoRefreshRetryAttempt += 1
|
|
323
|
+
const retrySeconds = Math.min(
|
|
324
|
+
AUTO_REFRESH_RETRY_MAX_SECONDS,
|
|
325
|
+
AUTO_REFRESH_RETRY_BASE_SECONDS * (2 ** (this.#autoRefreshRetryAttempt - 1))
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
this.#clearAutoRefreshTimeout()
|
|
329
|
+
this.#autoRefreshTimeout = setTimeout(() => {
|
|
330
|
+
this.#autoRefreshTimeout = null
|
|
331
|
+
if (this.#invalid) {
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.#refreshAccessToken().catch(() => {
|
|
336
|
+
// Errors are emitted by #performRefresh; retries are scheduled there as well.
|
|
337
|
+
})
|
|
338
|
+
}, retrySeconds * 1000)
|
|
339
|
+
|
|
340
|
+
this.#autoRefreshTimeout.unref?.()
|
|
341
|
+
}
|
|
342
|
+
|
|
203
343
|
/**
|
|
204
344
|
* Check if the token is currently valid (not expired and not marked invalid).
|
|
205
345
|
* @returns {boolean} True if token is valid
|
package/lib/yoto-account.d.ts
CHANGED
|
@@ -17,14 +17,168 @@
|
|
|
17
17
|
* @property {number} deviceCount - Number of devices managed
|
|
18
18
|
* @property {string[]} devices - Array of device IDs
|
|
19
19
|
*/
|
|
20
|
+
/**
|
|
21
|
+
* Device event wrapper
|
|
22
|
+
* @template T
|
|
23
|
+
* @typedef {{ deviceId: string } & T} YotoAccountDeviceEvent
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Device added event data
|
|
27
|
+
* @typedef {YotoAccountDeviceEvent<{}>} YotoAccountDeviceAddedEvent
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Device removed event data
|
|
31
|
+
* @typedef {YotoAccountDeviceEvent<{}>} YotoAccountDeviceRemovedEvent
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Device status update event data
|
|
35
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
36
|
+
* status: YotoDeviceStatus,
|
|
37
|
+
* source: string,
|
|
38
|
+
* changedFields: Set<keyof YotoDeviceStatus>
|
|
39
|
+
* }>} YotoAccountStatusUpdateEvent
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Device config update event data
|
|
43
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
44
|
+
* config: YotoDeviceModelConfig,
|
|
45
|
+
* changedFields: Set<keyof YotoDeviceModelConfig>
|
|
46
|
+
* }>} YotoAccountConfigUpdateEvent
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* Device playback update event data
|
|
50
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
51
|
+
* playback: YotoPlaybackState,
|
|
52
|
+
* changedFields: Set<keyof YotoPlaybackState>
|
|
53
|
+
* }>} YotoAccountPlaybackUpdateEvent
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Device online event data
|
|
57
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
58
|
+
* metadata: YotoDeviceOnlineMetadata
|
|
59
|
+
* }>} YotoAccountOnlineEvent
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* Device offline event data
|
|
63
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
64
|
+
* metadata: YotoDeviceOfflineMetadata
|
|
65
|
+
* }>} YotoAccountOfflineEvent
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* MQTT connect event data
|
|
69
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
70
|
+
* metadata: YotoMqttConnectMetadata
|
|
71
|
+
* }>} YotoAccountMqttConnectEvent
|
|
72
|
+
*/
|
|
73
|
+
/**
|
|
74
|
+
* MQTT disconnect event data
|
|
75
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
76
|
+
* metadata: YotoMqttDisconnectMetadata
|
|
77
|
+
* }>} YotoAccountMqttDisconnectEvent
|
|
78
|
+
*/
|
|
79
|
+
/**
|
|
80
|
+
* MQTT close event data
|
|
81
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
82
|
+
* metadata: YotoMqttCloseMetadata
|
|
83
|
+
* }>} YotoAccountMqttCloseEvent
|
|
84
|
+
*/
|
|
85
|
+
/**
|
|
86
|
+
* MQTT reconnect event data
|
|
87
|
+
* @typedef {YotoAccountDeviceEvent<{}>} YotoAccountMqttReconnectEvent
|
|
88
|
+
*/
|
|
89
|
+
/**
|
|
90
|
+
* MQTT offline event data
|
|
91
|
+
* @typedef {YotoAccountDeviceEvent<{}>} YotoAccountMqttOfflineEvent
|
|
92
|
+
*/
|
|
93
|
+
/**
|
|
94
|
+
* MQTT end event data
|
|
95
|
+
* @typedef {YotoAccountDeviceEvent<{}>} YotoAccountMqttEndEvent
|
|
96
|
+
*/
|
|
97
|
+
/**
|
|
98
|
+
* MQTT status event data
|
|
99
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
100
|
+
* topic: string,
|
|
101
|
+
* message: YotoStatusMessage
|
|
102
|
+
* }>} YotoAccountMqttStatusEvent
|
|
103
|
+
*/
|
|
104
|
+
/**
|
|
105
|
+
* MQTT events event data
|
|
106
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
107
|
+
* topic: string,
|
|
108
|
+
* message: YotoEventsMessage
|
|
109
|
+
* }>} YotoAccountMqttEventsEvent
|
|
110
|
+
*/
|
|
111
|
+
/**
|
|
112
|
+
* MQTT legacy status event data
|
|
113
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
114
|
+
* topic: string,
|
|
115
|
+
* message: YotoStatusLegacyMessage
|
|
116
|
+
* }>} YotoAccountMqttStatusLegacyEvent
|
|
117
|
+
*/
|
|
118
|
+
/**
|
|
119
|
+
* MQTT response event data
|
|
120
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
121
|
+
* topic: string,
|
|
122
|
+
* message: YotoResponseMessage
|
|
123
|
+
* }>} YotoAccountMqttResponseEvent
|
|
124
|
+
*/
|
|
125
|
+
/**
|
|
126
|
+
* MQTT unknown event data
|
|
127
|
+
* @typedef {YotoAccountDeviceEvent<{
|
|
128
|
+
* topic: string,
|
|
129
|
+
* message: unknown
|
|
130
|
+
* }>} YotoAccountMqttUnknownEvent
|
|
131
|
+
*/
|
|
132
|
+
/**
|
|
133
|
+
* Account error event data
|
|
134
|
+
* @typedef {Object} YotoAccountErrorEvent
|
|
135
|
+
* @property {Error} error - Error instance
|
|
136
|
+
* @property {YotoAccountErrorContext} context - Error context
|
|
137
|
+
*/
|
|
138
|
+
/**
|
|
139
|
+
* Device event handler set
|
|
140
|
+
* @typedef {Object} YotoAccountDeviceEventHandlers
|
|
141
|
+
* @property {(status: YotoDeviceStatus, source: string, changedFields: Set<keyof YotoDeviceStatus>) => void} statusUpdate
|
|
142
|
+
* @property {(config: YotoDeviceModelConfig, changedFields: Set<keyof YotoDeviceModelConfig>) => void} configUpdate
|
|
143
|
+
* @property {(playback: YotoPlaybackState, changedFields: Set<keyof YotoPlaybackState>) => void} playbackUpdate
|
|
144
|
+
* @property {(metadata: YotoDeviceOnlineMetadata) => void} online
|
|
145
|
+
* @property {(metadata: YotoDeviceOfflineMetadata) => void} offline
|
|
146
|
+
* @property {(metadata: YotoMqttConnectMetadata) => void} mqttConnect
|
|
147
|
+
* @property {(metadata: YotoMqttDisconnectMetadata) => void} mqttDisconnect
|
|
148
|
+
* @property {(metadata: YotoMqttCloseMetadata) => void} mqttClose
|
|
149
|
+
* @property {() => void} mqttReconnect
|
|
150
|
+
* @property {() => void} mqttOffline
|
|
151
|
+
* @property {() => void} mqttEnd
|
|
152
|
+
* @property {(topic: string, message: YotoStatusMessage) => void} mqttStatus
|
|
153
|
+
* @property {(topic: string, message: YotoEventsMessage) => void} mqttEvents
|
|
154
|
+
* @property {(topic: string, message: YotoStatusLegacyMessage) => void} mqttStatusLegacy
|
|
155
|
+
* @property {(topic: string, message: YotoResponseMessage) => void} mqttResponse
|
|
156
|
+
* @property {(topic: string, message: unknown) => void} mqttUnknown
|
|
157
|
+
*/
|
|
20
158
|
/**
|
|
21
159
|
* Event map for YotoAccount
|
|
22
160
|
* @typedef {{
|
|
23
161
|
* 'started': [YotoAccountStartedMetadata],
|
|
24
162
|
* 'stopped': [],
|
|
25
|
-
* 'deviceAdded': [
|
|
26
|
-
* 'deviceRemoved': [
|
|
27
|
-
* '
|
|
163
|
+
* 'deviceAdded': [YotoAccountDeviceAddedEvent],
|
|
164
|
+
* 'deviceRemoved': [YotoAccountDeviceRemovedEvent],
|
|
165
|
+
* 'statusUpdate': [YotoAccountStatusUpdateEvent],
|
|
166
|
+
* 'configUpdate': [YotoAccountConfigUpdateEvent],
|
|
167
|
+
* 'playbackUpdate': [YotoAccountPlaybackUpdateEvent],
|
|
168
|
+
* 'online': [YotoAccountOnlineEvent],
|
|
169
|
+
* 'offline': [YotoAccountOfflineEvent],
|
|
170
|
+
* 'mqttConnect': [YotoAccountMqttConnectEvent],
|
|
171
|
+
* 'mqttDisconnect': [YotoAccountMqttDisconnectEvent],
|
|
172
|
+
* 'mqttClose': [YotoAccountMqttCloseEvent],
|
|
173
|
+
* 'mqttReconnect': [YotoAccountMqttReconnectEvent],
|
|
174
|
+
* 'mqttOffline': [YotoAccountMqttOfflineEvent],
|
|
175
|
+
* 'mqttEnd': [YotoAccountMqttEndEvent],
|
|
176
|
+
* 'mqttStatus': [YotoAccountMqttStatusEvent],
|
|
177
|
+
* 'mqttEvents': [YotoAccountMqttEventsEvent],
|
|
178
|
+
* 'mqttStatusLegacy': [YotoAccountMqttStatusLegacyEvent],
|
|
179
|
+
* 'mqttResponse': [YotoAccountMqttResponseEvent],
|
|
180
|
+
* 'mqttUnknown': [YotoAccountMqttUnknownEvent],
|
|
181
|
+
* 'error': [YotoAccountErrorEvent]
|
|
28
182
|
* }} YotoAccountEventMap
|
|
29
183
|
*/
|
|
30
184
|
/**
|
|
@@ -33,9 +187,10 @@
|
|
|
33
187
|
* Events:
|
|
34
188
|
* - 'started' - Emitted when account starts, passes metadata with deviceCount and devices array
|
|
35
189
|
* - 'stopped' - Emitted when account stops
|
|
36
|
-
* - 'deviceAdded' - Emitted when a device is added, passes
|
|
37
|
-
* - 'deviceRemoved' - Emitted when a device is removed, passes deviceId
|
|
38
|
-
* -
|
|
190
|
+
* - 'deviceAdded' - Emitted when a device is added, passes { deviceId }
|
|
191
|
+
* - 'deviceRemoved' - Emitted when a device is removed, passes { deviceId }
|
|
192
|
+
* - Device events are re-emitted with device context, see event map for signatures
|
|
193
|
+
* - 'error' - Emitted when an error occurs, passes { error, context }
|
|
39
194
|
*
|
|
40
195
|
* Note: To listen to individual device events (statusUpdate, configUpdate, playbackUpdate, online, offline, etc.),
|
|
41
196
|
* access the device models directly via account.devices or account.getDevice(deviceId) and attach listeners.
|
|
@@ -145,19 +300,194 @@ export type YotoAccountStartedMetadata = {
|
|
|
145
300
|
*/
|
|
146
301
|
devices: string[];
|
|
147
302
|
};
|
|
303
|
+
/**
|
|
304
|
+
* Device event wrapper
|
|
305
|
+
*/
|
|
306
|
+
export type YotoAccountDeviceEvent<T> = {
|
|
307
|
+
deviceId: string;
|
|
308
|
+
} & T;
|
|
309
|
+
/**
|
|
310
|
+
* Device added event data
|
|
311
|
+
*/
|
|
312
|
+
export type YotoAccountDeviceAddedEvent = YotoAccountDeviceEvent<{}>;
|
|
313
|
+
/**
|
|
314
|
+
* Device removed event data
|
|
315
|
+
*/
|
|
316
|
+
export type YotoAccountDeviceRemovedEvent = YotoAccountDeviceEvent<{}>;
|
|
317
|
+
/**
|
|
318
|
+
* Device status update event data
|
|
319
|
+
*/
|
|
320
|
+
export type YotoAccountStatusUpdateEvent = YotoAccountDeviceEvent<{
|
|
321
|
+
status: YotoDeviceStatus;
|
|
322
|
+
source: string;
|
|
323
|
+
changedFields: Set<keyof YotoDeviceStatus>;
|
|
324
|
+
}>;
|
|
325
|
+
/**
|
|
326
|
+
* Device config update event data
|
|
327
|
+
*/
|
|
328
|
+
export type YotoAccountConfigUpdateEvent = YotoAccountDeviceEvent<{
|
|
329
|
+
config: YotoDeviceModelConfig;
|
|
330
|
+
changedFields: Set<keyof YotoDeviceModelConfig>;
|
|
331
|
+
}>;
|
|
332
|
+
/**
|
|
333
|
+
* Device playback update event data
|
|
334
|
+
*/
|
|
335
|
+
export type YotoAccountPlaybackUpdateEvent = YotoAccountDeviceEvent<{
|
|
336
|
+
playback: YotoPlaybackState;
|
|
337
|
+
changedFields: Set<keyof YotoPlaybackState>;
|
|
338
|
+
}>;
|
|
339
|
+
/**
|
|
340
|
+
* Device online event data
|
|
341
|
+
*/
|
|
342
|
+
export type YotoAccountOnlineEvent = YotoAccountDeviceEvent<{
|
|
343
|
+
metadata: YotoDeviceOnlineMetadata;
|
|
344
|
+
}>;
|
|
345
|
+
/**
|
|
346
|
+
* Device offline event data
|
|
347
|
+
*/
|
|
348
|
+
export type YotoAccountOfflineEvent = YotoAccountDeviceEvent<{
|
|
349
|
+
metadata: YotoDeviceOfflineMetadata;
|
|
350
|
+
}>;
|
|
351
|
+
/**
|
|
352
|
+
* MQTT connect event data
|
|
353
|
+
*/
|
|
354
|
+
export type YotoAccountMqttConnectEvent = YotoAccountDeviceEvent<{
|
|
355
|
+
metadata: YotoMqttConnectMetadata;
|
|
356
|
+
}>;
|
|
357
|
+
/**
|
|
358
|
+
* MQTT disconnect event data
|
|
359
|
+
*/
|
|
360
|
+
export type YotoAccountMqttDisconnectEvent = YotoAccountDeviceEvent<{
|
|
361
|
+
metadata: YotoMqttDisconnectMetadata;
|
|
362
|
+
}>;
|
|
363
|
+
/**
|
|
364
|
+
* MQTT close event data
|
|
365
|
+
*/
|
|
366
|
+
export type YotoAccountMqttCloseEvent = YotoAccountDeviceEvent<{
|
|
367
|
+
metadata: YotoMqttCloseMetadata;
|
|
368
|
+
}>;
|
|
369
|
+
/**
|
|
370
|
+
* MQTT reconnect event data
|
|
371
|
+
*/
|
|
372
|
+
export type YotoAccountMqttReconnectEvent = YotoAccountDeviceEvent<{}>;
|
|
373
|
+
/**
|
|
374
|
+
* MQTT offline event data
|
|
375
|
+
*/
|
|
376
|
+
export type YotoAccountMqttOfflineEvent = YotoAccountDeviceEvent<{}>;
|
|
377
|
+
/**
|
|
378
|
+
* MQTT end event data
|
|
379
|
+
*/
|
|
380
|
+
export type YotoAccountMqttEndEvent = YotoAccountDeviceEvent<{}>;
|
|
381
|
+
/**
|
|
382
|
+
* MQTT status event data
|
|
383
|
+
*/
|
|
384
|
+
export type YotoAccountMqttStatusEvent = YotoAccountDeviceEvent<{
|
|
385
|
+
topic: string;
|
|
386
|
+
message: YotoStatusMessage;
|
|
387
|
+
}>;
|
|
388
|
+
/**
|
|
389
|
+
* MQTT events event data
|
|
390
|
+
*/
|
|
391
|
+
export type YotoAccountMqttEventsEvent = YotoAccountDeviceEvent<{
|
|
392
|
+
topic: string;
|
|
393
|
+
message: YotoEventsMessage;
|
|
394
|
+
}>;
|
|
395
|
+
/**
|
|
396
|
+
* MQTT legacy status event data
|
|
397
|
+
*/
|
|
398
|
+
export type YotoAccountMqttStatusLegacyEvent = YotoAccountDeviceEvent<{
|
|
399
|
+
topic: string;
|
|
400
|
+
message: YotoStatusLegacyMessage;
|
|
401
|
+
}>;
|
|
402
|
+
/**
|
|
403
|
+
* MQTT response event data
|
|
404
|
+
*/
|
|
405
|
+
export type YotoAccountMqttResponseEvent = YotoAccountDeviceEvent<{
|
|
406
|
+
topic: string;
|
|
407
|
+
message: YotoResponseMessage;
|
|
408
|
+
}>;
|
|
409
|
+
/**
|
|
410
|
+
* MQTT unknown event data
|
|
411
|
+
*/
|
|
412
|
+
export type YotoAccountMqttUnknownEvent = YotoAccountDeviceEvent<{
|
|
413
|
+
topic: string;
|
|
414
|
+
message: unknown;
|
|
415
|
+
}>;
|
|
416
|
+
/**
|
|
417
|
+
* Account error event data
|
|
418
|
+
*/
|
|
419
|
+
export type YotoAccountErrorEvent = {
|
|
420
|
+
/**
|
|
421
|
+
* - Error instance
|
|
422
|
+
*/
|
|
423
|
+
error: Error;
|
|
424
|
+
/**
|
|
425
|
+
* - Error context
|
|
426
|
+
*/
|
|
427
|
+
context: YotoAccountErrorContext;
|
|
428
|
+
};
|
|
429
|
+
/**
|
|
430
|
+
* Device event handler set
|
|
431
|
+
*/
|
|
432
|
+
export type YotoAccountDeviceEventHandlers = {
|
|
433
|
+
statusUpdate: (status: YotoDeviceStatus, source: string, changedFields: Set<keyof YotoDeviceStatus>) => void;
|
|
434
|
+
configUpdate: (config: YotoDeviceModelConfig, changedFields: Set<keyof YotoDeviceModelConfig>) => void;
|
|
435
|
+
playbackUpdate: (playback: YotoPlaybackState, changedFields: Set<keyof YotoPlaybackState>) => void;
|
|
436
|
+
online: (metadata: YotoDeviceOnlineMetadata) => void;
|
|
437
|
+
offline: (metadata: YotoDeviceOfflineMetadata) => void;
|
|
438
|
+
mqttConnect: (metadata: YotoMqttConnectMetadata) => void;
|
|
439
|
+
mqttDisconnect: (metadata: YotoMqttDisconnectMetadata) => void;
|
|
440
|
+
mqttClose: (metadata: YotoMqttCloseMetadata) => void;
|
|
441
|
+
mqttReconnect: () => void;
|
|
442
|
+
mqttOffline: () => void;
|
|
443
|
+
mqttEnd: () => void;
|
|
444
|
+
mqttStatus: (topic: string, message: YotoStatusMessage) => void;
|
|
445
|
+
mqttEvents: (topic: string, message: YotoEventsMessage) => void;
|
|
446
|
+
mqttStatusLegacy: (topic: string, message: YotoStatusLegacyMessage) => void;
|
|
447
|
+
mqttResponse: (topic: string, message: YotoResponseMessage) => void;
|
|
448
|
+
mqttUnknown: (topic: string, message: unknown) => void;
|
|
449
|
+
};
|
|
148
450
|
/**
|
|
149
451
|
* Event map for YotoAccount
|
|
150
452
|
*/
|
|
151
453
|
export type YotoAccountEventMap = {
|
|
152
454
|
"started": [YotoAccountStartedMetadata];
|
|
153
455
|
"stopped": [];
|
|
154
|
-
"deviceAdded": [
|
|
155
|
-
"deviceRemoved": [
|
|
156
|
-
"
|
|
456
|
+
"deviceAdded": [YotoAccountDeviceAddedEvent];
|
|
457
|
+
"deviceRemoved": [YotoAccountDeviceRemovedEvent];
|
|
458
|
+
"statusUpdate": [YotoAccountStatusUpdateEvent];
|
|
459
|
+
"configUpdate": [YotoAccountConfigUpdateEvent];
|
|
460
|
+
"playbackUpdate": [YotoAccountPlaybackUpdateEvent];
|
|
461
|
+
"online": [YotoAccountOnlineEvent];
|
|
462
|
+
"offline": [YotoAccountOfflineEvent];
|
|
463
|
+
"mqttConnect": [YotoAccountMqttConnectEvent];
|
|
464
|
+
"mqttDisconnect": [YotoAccountMqttDisconnectEvent];
|
|
465
|
+
"mqttClose": [YotoAccountMqttCloseEvent];
|
|
466
|
+
"mqttReconnect": [YotoAccountMqttReconnectEvent];
|
|
467
|
+
"mqttOffline": [YotoAccountMqttOfflineEvent];
|
|
468
|
+
"mqttEnd": [YotoAccountMqttEndEvent];
|
|
469
|
+
"mqttStatus": [YotoAccountMqttStatusEvent];
|
|
470
|
+
"mqttEvents": [YotoAccountMqttEventsEvent];
|
|
471
|
+
"mqttStatusLegacy": [YotoAccountMqttStatusLegacyEvent];
|
|
472
|
+
"mqttResponse": [YotoAccountMqttResponseEvent];
|
|
473
|
+
"mqttUnknown": [YotoAccountMqttUnknownEvent];
|
|
474
|
+
"error": [YotoAccountErrorEvent];
|
|
157
475
|
};
|
|
158
476
|
import { EventEmitter } from 'events';
|
|
159
477
|
import { YotoClient } from './api-client.js';
|
|
160
478
|
import { YotoDeviceModel } from './yoto-device.js';
|
|
161
479
|
import type { YotoClientConstructorOptions } from './api-client.js';
|
|
162
480
|
import type { YotoDeviceModelOptions } from './yoto-device.js';
|
|
481
|
+
import type { YotoDeviceStatus } from './yoto-device.js';
|
|
482
|
+
import type { YotoDeviceModelConfig } from './yoto-device.js';
|
|
483
|
+
import type { YotoPlaybackState } from './yoto-device.js';
|
|
484
|
+
import type { YotoDeviceOnlineMetadata } from './yoto-device.js';
|
|
485
|
+
import type { YotoDeviceOfflineMetadata } from './yoto-device.js';
|
|
486
|
+
import type { YotoMqttConnectMetadata } from './yoto-device.js';
|
|
487
|
+
import type { YotoMqttDisconnectMetadata } from './yoto-device.js';
|
|
488
|
+
import type { YotoMqttCloseMetadata } from './yoto-device.js';
|
|
489
|
+
import type { YotoStatusMessage } from './mqtt/client.js';
|
|
490
|
+
import type { YotoEventsMessage } from './mqtt/client.js';
|
|
491
|
+
import type { YotoStatusLegacyMessage } from './mqtt/client.js';
|
|
492
|
+
import type { YotoResponseMessage } from './mqtt/client.js';
|
|
163
493
|
//# sourceMappingURL=yoto-account.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yoto-account.d.ts","sourceRoot":"","sources":["yoto-account.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"yoto-account.d.ts","sourceRoot":"","sources":["yoto-account.js"],"names":[],"mappings":"AAcA;;;;;GAKG;AAEH;;;;;;GAMG;AAEH;;;;;GAKG;AAEH;;;;GAIG;AAEH;;;GAGG;AAEH;;;GAGG;AAEH;;;;;;;GAOG;AAEH;;;;;;GAMG;AAEH;;;;;;GAMG;AAEH;;;;;GAKG;AAEH;;;;;GAKG;AAEH;;;;;GAKG;AAEH;;;;;GAKG;AAEH;;;;;GAKG;AAEH;;;GAGG;AAEH;;;GAGG;AAEH;;;GAGG;AAEH;;;;;;GAMG;AAEH;;;;;;GAMG;AAEH;;;;;;GAMG;AAEH;;;;;;GAMG;AAEH;;;;;;GAMG;AAEH;;;;;GAKG;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAMH;;;;;;;;;;;;;;;GAeG;AACH;IAQE;;;QAGI;IACJ,qBAFY,kBAAkB,EAS7B;IAMD;;;OAGG;IACH,cAFa,UAAU,CAEc;IAErC;;;OAGG;IACH,eAFa,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAEF;IAEvC;;;OAGG;IACH,eAFa,OAAO,CAEmB;IAEvC;;;OAGG;IACH,mBAFa,OAAO,CAE2B;IAE/C;;;;OAIG;IACH,oBAHW,MAAM,GACJ,eAAe,GAAG,SAAS,CAEmB;IAE3D;;;OAGG;IACH,gBAFa,MAAM,EAAE,CAEsC;IAM3D;;;;OAIG;IACH,SAHa,OAAO,CAAC,IAAI,CAAC,CAkEzB;IAED;;;OAGG;IACH,QAFa,OAAO,CAAC,IAAI,CAAC,CAgDzB;IAED;;;OAGG;IACH,WAFa,OAAO,CAAC,IAAI,CAAC,CAKzB;IAED;;;OAGG;IACH,kBAFa,OAAO,CAAC,IAAI,CAAC,CAqFzB;;CAyLF;;;;;;;;mBAtrBa,4BAA4B;;;;mBAC5B,sBAAsB;;;;;;;;;YAMtB,MAAM;;;;eACN,MAAM;;;;gBACN,MAAM;;;;;;;;;iBAMN,MAAM;;;;aACN,MAAM,EAAE;;;;;mCAKT,CAAC,IACD;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,CAAC;;;;0CAKxB,sBAAsB,CAAC,EAAE,CAAC;;;;4CAK1B,sBAAsB,CAAC,EAAE,CAAC;;;;2CAK1B,sBAAsB,CAAC;IAC/B,MAAM,EAAE,gBAAgB,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,GAAG,CAAC,MAAM,gBAAgB,CAAC,CAAA;CAC3C,CAAC;;;;2CAKQ,sBAAsB,CAAC;IAC/B,MAAM,EAAE,qBAAqB,CAAC;IAC9B,aAAa,EAAE,GAAG,CAAC,MAAM,qBAAqB,CAAC,CAAA;CAChD,CAAC;;;;6CAKQ,sBAAsB,CAAC;IAC/B,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,aAAa,EAAE,GAAG,CAAC,MAAM,iBAAiB,CAAC,CAAA;CAC5C,CAAC;;;;qCAKQ,sBAAsB,CAAC;IAC/B,QAAQ,EAAE,wBAAwB,CAAA;CACnC,CAAC;;;;sCAKQ,sBAAsB,CAAC;IAC/B,QAAQ,EAAE,yBAAyB,CAAA;CACpC,CAAC;;;;0CAKQ,sBAAsB,CAAC;IAC/B,QAAQ,EAAE,uBAAuB,CAAA;CAClC,CAAC;;;;6CAKQ,sBAAsB,CAAC;IAC/B,QAAQ,EAAE,0BAA0B,CAAA;CACrC,CAAC;;;;wCAKQ,sBAAsB,CAAC;IAC/B,QAAQ,EAAE,qBAAqB,CAAA;CAChC,CAAC;;;;4CAKQ,sBAAsB,CAAC,EAAE,CAAC;;;;0CAK1B,sBAAsB,CAAC,EAAE,CAAC;;;;sCAK1B,sBAAsB,CAAC,EAAE,CAAC;;;;yCAK1B,sBAAsB,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,iBAAiB,CAAA;CAC3B,CAAC;;;;yCAKQ,sBAAsB,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,iBAAiB,CAAA;CAC3B,CAAC;;;;+CAKQ,sBAAsB,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,uBAAuB,CAAA;CACjC,CAAC;;;;2CAKQ,sBAAsB,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,mBAAmB,CAAA;CAC7B,CAAC;;;;0CAKQ,sBAAsB,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAA;CACjB,CAAC;;;;;;;;WAMS,KAAK;;;;aACL,uBAAuB;;;;;;kBAMvB,CAAC,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,gBAAgB,CAAC,KAAK,IAAI;kBAC9F,CAAC,MAAM,EAAE,qBAAqB,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,qBAAqB,CAAC,KAAK,IAAI;oBACxF,CAAC,QAAQ,EAAE,iBAAiB,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,iBAAiB,CAAC,KAAK,IAAI;YAClF,CAAC,QAAQ,EAAE,wBAAwB,KAAK,IAAI;aAC5C,CAAC,QAAQ,EAAE,yBAAyB,KAAK,IAAI;iBAC7C,CAAC,QAAQ,EAAE,uBAAuB,KAAK,IAAI;oBAC3C,CAAC,QAAQ,EAAE,0BAA0B,KAAK,IAAI;eAC9C,CAAC,QAAQ,EAAE,qBAAqB,KAAK,IAAI;mBACzC,MAAM,IAAI;iBACV,MAAM,IAAI;aACV,MAAM,IAAI;gBACV,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,KAAK,IAAI;gBACnD,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,KAAK,IAAI;sBACnD,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,KAAK,IAAI;kBACzD,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,KAAK,IAAI;iBACrD,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI;;;;;kCAK1C;IACZ,SAAa,EAAE,CAAC,0BAA0B,CAAC,CAAC;IAC5C,SAAa,EAAE,EAAE,CAAC;IAClB,aAAiB,EAAE,CAAC,2BAA2B,CAAC,CAAC;IACjD,eAAmB,EAAE,CAAC,6BAA6B,CAAC,CAAC;IACrD,cAAkB,EAAE,CAAC,4BAA4B,CAAC,CAAC;IACnD,cAAkB,EAAE,CAAC,4BAA4B,CAAC,CAAC;IACnD,gBAAoB,EAAE,CAAC,8BAA8B,CAAC,CAAC;IACvD,QAAY,EAAE,CAAC,sBAAsB,CAAC,CAAC;IACvC,SAAa,EAAE,CAAC,uBAAuB,CAAC,CAAC;IACzC,aAAiB,EAAE,CAAC,2BAA2B,CAAC,CAAC;IACjD,gBAAoB,EAAE,CAAC,8BAA8B,CAAC,CAAC;IACvD,WAAe,EAAE,CAAC,yBAAyB,CAAC,CAAC;IAC7C,eAAmB,EAAE,CAAC,6BAA6B,CAAC,CAAC;IACrD,aAAiB,EAAE,CAAC,2BAA2B,CAAC,CAAC;IACjD,SAAa,EAAE,CAAC,uBAAuB,CAAC,CAAC;IACzC,YAAgB,EAAE,CAAC,0BAA0B,CAAC,CAAC;IAC/C,YAAgB,EAAE,CAAC,0BAA0B,CAAC,CAAC;IAC/C,kBAAsB,EAAE,CAAC,gCAAgC,CAAC,CAAC;IAC3D,cAAkB,EAAE,CAAC,4BAA4B,CAAC,CAAC;IACnD,aAAiB,EAAE,CAAC,2BAA2B,CAAC,CAAC;IACjD,OAAW,EAAE,CAAC,qBAAqB,CAAC,CAAA;CACjC;6BArNyB,QAAQ;2BACV,iBAAiB;gCACZ,kBAAkB;kDAPD,iBAAiB;4CACsK,kBAAkB;sCAAlB,kBAAkB;2CAAlB,kBAAkB;uCAAlB,kBAAkB;8CAAlB,kBAAkB;+CAAlB,kBAAkB;6CAAlB,kBAAkB;gDAAlB,kBAAkB;2CAAlB,kBAAkB;uCACnJ,kBAAkB;uCAAlB,kBAAkB;6CAAlB,kBAAkB;yCAAlB,kBAAkB"}
|