yoto-nodejs-client 0.0.8 → 0.0.9

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/bin/content.js CHANGED
File without changes
package/bin/devices.js CHANGED
File without changes
package/bin/groups.js CHANGED
File without changes
package/bin/icons.js CHANGED
File without changes
File without changes
package/lib/pkg.d.cts CHANGED
@@ -7,13 +7,15 @@ export const pkg: {
7
7
  url: string;
8
8
  };
9
9
  dependencies: {
10
+ fastq: string;
10
11
  "jwt-decode": string;
11
12
  mqtt: string;
13
+ "quick-lru": string;
12
14
  undici: string;
13
15
  };
14
16
  devDependencies: {
15
- "@unblessed/node": string;
16
17
  "@types/node": string;
18
+ "@unblessed/node": string;
17
19
  "@voxpelli/tsconfig": string;
18
20
  argsclopts: string;
19
21
  "auto-changelog": string;
@@ -116,6 +116,34 @@ export const YotoDeviceModelConfigType: {};
116
116
  * @property {string} updatedAt - ISO 8601 timestamp of last update
117
117
  */
118
118
  export const YotoPlaybackStateType: {};
119
+ /**
120
+ * Cached chapter info for playback enrichment.
121
+ * @typedef {Object} YotoCardCacheChapterInfo
122
+ * @property {string} key
123
+ * @property {string} title
124
+ */
125
+ /**
126
+ * Cached track info for playback enrichment.
127
+ * @typedef {Object} YotoCardCacheTrackInfo
128
+ * @property {string} key
129
+ * @property {string} title
130
+ * @property {number} duration
131
+ * @property {string} chapterKey
132
+ * @property {string} chapterTitle
133
+ */
134
+ /**
135
+ * Cached card metadata for playback enrichment.
136
+ * @typedef {Object} YotoCardCacheEntry
137
+ * @property {string} cardId
138
+ * @property {string} title
139
+ * @property {Map<string, YotoCardCacheChapterInfo>} chaptersByKey
140
+ * @property {Map<string, YotoCardCacheTrackInfo>} tracksByKey
141
+ */
142
+ /**
143
+ * Cached card lookup task.
144
+ * @typedef {Object} YotoCardCacheTask
145
+ * @property {string} cardId
146
+ */
119
147
  /**
120
148
  * Complete device client state
121
149
  * @typedef {Object} YotoDeviceClientState
@@ -845,6 +873,38 @@ export type YotoPlaybackState = {
845
873
  */
846
874
  updatedAt: string;
847
875
  };
876
+ /**
877
+ * Cached chapter info for playback enrichment.
878
+ */
879
+ export type YotoCardCacheChapterInfo = {
880
+ key: string;
881
+ title: string;
882
+ };
883
+ /**
884
+ * Cached track info for playback enrichment.
885
+ */
886
+ export type YotoCardCacheTrackInfo = {
887
+ key: string;
888
+ title: string;
889
+ duration: number;
890
+ chapterKey: string;
891
+ chapterTitle: string;
892
+ };
893
+ /**
894
+ * Cached card metadata for playback enrichment.
895
+ */
896
+ export type YotoCardCacheEntry = {
897
+ cardId: string;
898
+ title: string;
899
+ chaptersByKey: Map<string, YotoCardCacheChapterInfo>;
900
+ tracksByKey: Map<string, YotoCardCacheTrackInfo>;
901
+ };
902
+ /**
903
+ * Cached card lookup task.
904
+ */
905
+ export type YotoCardCacheTask = {
906
+ cardId: string;
907
+ };
848
908
  /**
849
909
  * Complete device client state
850
910
  */
@@ -1 +1 @@
1
- {"version":3,"file":"yoto-device.d.ts","sourceRoot":"","sources":["yoto-device.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"yoto-device.d.ts","sourceRoot":"","sources":["yoto-device.js"],"names":[],"mappings":"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"}
@@ -8,7 +8,9 @@
8
8
 
9
9
  /**
10
10
  * @import { IConnackPacket } from 'mqtt'
11
+ * @import { queueAsPromised } from 'fastq'
11
12
  * @import { YotoClient } from './api-client.js'
13
+ * @import { YotoCard } from './api-endpoints/content.js'
12
14
  * @import { YotoDevice, YotoDeviceConfig, YotoDeviceShortcuts, YotoDeviceFullStatus, YotoDeviceStatusResponse, YotoDeviceCommand, YotoDeviceCommandResponse } from './api-endpoints/devices.js'
13
15
  * @import { YotoMqttClient, YotoMqttStatus, YotoEventsMessage, YotoLegacyStatus, YotoStatusMessage, YotoStatusLegacyMessage, YotoResponseMessage, YotoMqttClientDisconnectMetadata, YotoMqttClientCloseMetadata, PlaybackStatus } from './mqtt/client.js'
14
16
  * @import { YotoMqttOptions } from './mqtt/factory.js'
@@ -16,10 +18,16 @@
16
18
  */
17
19
 
18
20
  import { EventEmitter } from 'events'
21
+ import fastq from 'fastq'
22
+ import QuickLRU from 'quick-lru'
19
23
  import { parseTemperature } from './helpers/temperature.js'
20
24
  import { detectPowerState } from './helpers/power-state.js'
21
25
  import { typedKeys } from './helpers/typed-keys.js'
22
26
 
27
+ const CARD_CACHE_MAX_SIZE = 2000
28
+ const CARD_CACHE_QUEUE_CONCURRENCY = 1
29
+ const CARD_ID_NONE = 'none'
30
+
23
31
  // ============================================================================
24
32
  // Type Definitions
25
33
  // ============================================================================
@@ -410,6 +418,38 @@ export const YotoPlaybackStateType = {}
410
418
  // Internal Types
411
419
  // ============================================================================
412
420
 
421
+ /**
422
+ * Cached chapter info for playback enrichment.
423
+ * @typedef {Object} YotoCardCacheChapterInfo
424
+ * @property {string} key
425
+ * @property {string} title
426
+ */
427
+
428
+ /**
429
+ * Cached track info for playback enrichment.
430
+ * @typedef {Object} YotoCardCacheTrackInfo
431
+ * @property {string} key
432
+ * @property {string} title
433
+ * @property {number} duration
434
+ * @property {string} chapterKey
435
+ * @property {string} chapterTitle
436
+ */
437
+
438
+ /**
439
+ * Cached card metadata for playback enrichment.
440
+ * @typedef {Object} YotoCardCacheEntry
441
+ * @property {string} cardId
442
+ * @property {string} title
443
+ * @property {Map<string, YotoCardCacheChapterInfo>} chaptersByKey
444
+ * @property {Map<string, YotoCardCacheTrackInfo>} tracksByKey
445
+ */
446
+
447
+ /**
448
+ * Cached card lookup task.
449
+ * @typedef {Object} YotoCardCacheTask
450
+ * @property {string} cardId
451
+ */
452
+
413
453
  /**
414
454
  * Complete device client state
415
455
  * @typedef {Object} YotoDeviceClientState
@@ -567,6 +607,9 @@ export class YotoDeviceModel extends EventEmitter {
567
607
  /** @type {NodeJS.Timeout | null} */ #eventsRequestTimer = null
568
608
  /** @type {NodeJS.Timeout | null} */ #backgroundPollTimer = null
569
609
  /** @type {number | null} */ #shutdownDetectedAt = null
610
+ /** @type {QuickLRU<string, YotoCardCacheEntry>} */ #cardCache = new QuickLRU({ maxSize: CARD_CACHE_MAX_SIZE })
611
+ /** @type {queueAsPromised<YotoCardCacheTask, YotoCardCacheEntry | null>} */ #cardCacheQueue = fastq.promise(this, this.#fetchCardCacheTask, CARD_CACHE_QUEUE_CONCURRENCY)
612
+ /** @type {Set<string>} */ #cardCacheQueueTasks = new Set()
570
613
 
571
614
  /**
572
615
  * Create a Yoto device client
@@ -3102,6 +3145,69 @@ export class YotoDeviceModel extends EventEmitter {
3102
3145
  }
3103
3146
  }
3104
3147
 
3148
+ /**
3149
+ * Apply cached card metadata to playback state (if available).
3150
+ * @param {string | null} cardId
3151
+ * @param {YotoPlaybackState} playback
3152
+ * @param {Set<keyof YotoPlaybackState>} playbackChangedFields
3153
+ * @returns {boolean}
3154
+ */
3155
+ #applyCardCacheToPlayback (cardId, playback, playbackChangedFields) {
3156
+ if (!isCacheableCardId(cardId)) {
3157
+ return false
3158
+ }
3159
+
3160
+ const cachedEntry = this.#cardCache.get(cardId)
3161
+ if (cachedEntry) {
3162
+ return applyCardCacheToPlayback(cachedEntry, playback, playbackChangedFields)
3163
+ }
3164
+
3165
+ this.#queueCardCacheFetch(cardId)
3166
+ return false
3167
+ }
3168
+
3169
+ /**
3170
+ * @param {YotoCardCacheTask} task
3171
+ * @returns {Promise<YotoCardCacheEntry | null>}
3172
+ */
3173
+ async #fetchCardCacheTask (task) {
3174
+ try {
3175
+ const response = await this.#client.getContent({ cardId: task.cardId })
3176
+ const entry = buildCardCacheEntry(response.card)
3177
+ this.#cardCache.set(task.cardId, entry)
3178
+
3179
+ if (this.#state.playback.cardId === task.cardId) {
3180
+ /** @type {Set<keyof YotoPlaybackState>} */
3181
+ const playbackChangedFields = new Set()
3182
+ if (applyCardCacheToPlayback(entry, this.#state.playback, playbackChangedFields)) {
3183
+ this.#state.playback.updatedAt = new Date().toISOString()
3184
+ this.#state.lastUpdate.playback = Date.now()
3185
+ this.emit('playbackUpdate', this.playback, playbackChangedFields)
3186
+ }
3187
+ }
3188
+
3189
+ return entry
3190
+ } catch {
3191
+ return null
3192
+ } finally {
3193
+ this.#cardCacheQueueTasks.delete(task.cardId)
3194
+ }
3195
+ }
3196
+
3197
+ /**
3198
+ * Fetch card content for playback enrichment and cache the result.
3199
+ * @param {string} cardId
3200
+ * @returns {void}
3201
+ */
3202
+ #queueCardCacheFetch (cardId) {
3203
+ if (this.#cardCache.has(cardId) || this.#cardCacheQueueTasks.has(cardId)) {
3204
+ return
3205
+ }
3206
+
3207
+ this.#cardCacheQueueTasks.add(cardId)
3208
+ this.#cardCacheQueue.push({ cardId })
3209
+ }
3210
+
3105
3211
  /**
3106
3212
  * Handle MQTT event message - updates status, config, and playback
3107
3213
  * Events are partial updates - only changed fields are included
@@ -3123,6 +3229,7 @@ export class YotoDeviceModel extends EventEmitter {
3123
3229
  let playbackChanged = false
3124
3230
 
3125
3231
  const { status, config, playback } = this.#state
3232
+ const previousCardId = playback.cardId
3126
3233
  /** @type {Set<keyof YotoDeviceStatus>} */
3127
3234
  const statusChangedFields = new Set()
3128
3235
  /** @type {Set<keyof YotoDeviceModelConfig>} */
@@ -3204,10 +3311,13 @@ export class YotoDeviceModel extends EventEmitter {
3204
3311
  break
3205
3312
  }
3206
3313
  case 'cardId': {
3207
- if (eventsMessage.cardId !== undefined && playback.cardId !== eventsMessage.cardId) {
3208
- playback.cardId = eventsMessage.cardId
3209
- playbackChangedFields.add('cardId')
3210
- playbackChanged = true
3314
+ if (eventsMessage.cardId !== undefined) {
3315
+ const normalizedCardId = eventsMessage.cardId === CARD_ID_NONE ? null : eventsMessage.cardId
3316
+ if (playback.cardId !== normalizedCardId) {
3317
+ playback.cardId = normalizedCardId
3318
+ playbackChangedFields.add('cardId')
3319
+ playbackChanged = true
3320
+ }
3211
3321
  }
3212
3322
  break
3213
3323
  }
@@ -3286,6 +3396,12 @@ export class YotoDeviceModel extends EventEmitter {
3286
3396
  handleField(key, eventsMessage)
3287
3397
  }
3288
3398
 
3399
+ if (previousCardId !== playback.cardId) {
3400
+ if (this.#applyCardCacheToPlayback(playback.cardId, playback, playbackChangedFields)) {
3401
+ playbackChanged = true
3402
+ }
3403
+ }
3404
+
3289
3405
  // Update timestamps and emit events for changed categories
3290
3406
  if (statusChanged) {
3291
3407
  this.#state.lastUpdate.status = Date.now()
@@ -3332,6 +3448,98 @@ function createEmptyPlaybackState () {
3332
3448
  }
3333
3449
  }
3334
3450
 
3451
+ /**
3452
+ * @param {string | null} cardId
3453
+ * @returns {cardId is string}
3454
+ */
3455
+ function isCacheableCardId (cardId) {
3456
+ return typeof cardId === 'string' && cardId.length > 0 && cardId !== CARD_ID_NONE
3457
+ }
3458
+
3459
+ /**
3460
+ * @param {YotoCard} card
3461
+ * @returns {YotoCardCacheEntry}
3462
+ */
3463
+ function buildCardCacheEntry (card) {
3464
+ const chaptersByKey = new Map()
3465
+ const tracksByKey = new Map()
3466
+
3467
+ for (const chapter of card.content.chapters) {
3468
+ chaptersByKey.set(chapter.key, {
3469
+ key: chapter.key,
3470
+ title: chapter.title
3471
+ })
3472
+
3473
+ for (const track of chapter.tracks) {
3474
+ tracksByKey.set(track.key, {
3475
+ key: track.key,
3476
+ title: track.title,
3477
+ duration: track.duration,
3478
+ chapterKey: chapter.key,
3479
+ chapterTitle: chapter.title
3480
+ })
3481
+ }
3482
+ }
3483
+
3484
+ return {
3485
+ cardId: card.cardId,
3486
+ title: card.title,
3487
+ chaptersByKey,
3488
+ tracksByKey
3489
+ }
3490
+ }
3491
+
3492
+ /**
3493
+ * @param {YotoCardCacheEntry} entry
3494
+ * @param {YotoPlaybackState} playback
3495
+ * @param {Set<keyof YotoPlaybackState>} changedFields
3496
+ * @returns {boolean}
3497
+ */
3498
+ function applyCardCacheToPlayback (entry, playback, changedFields) {
3499
+ let changed = false
3500
+ const { trackKey } = playback
3501
+
3502
+ if (trackKey) {
3503
+ const trackInfo = entry.tracksByKey.get(trackKey)
3504
+ if (trackInfo) {
3505
+ if (playback.trackTitle !== trackInfo.title) {
3506
+ playback.trackTitle = trackInfo.title
3507
+ changedFields.add('trackTitle')
3508
+ changed = true
3509
+ }
3510
+
3511
+ if (playback.trackLength !== trackInfo.duration) {
3512
+ playback.trackLength = trackInfo.duration
3513
+ changedFields.add('trackLength')
3514
+ changed = true
3515
+ }
3516
+
3517
+ if (playback.chapterKey !== trackInfo.chapterKey) {
3518
+ playback.chapterKey = trackInfo.chapterKey
3519
+ changedFields.add('chapterKey')
3520
+ changed = true
3521
+ }
3522
+
3523
+ if (playback.chapterKey === trackInfo.chapterKey && playback.chapterTitle !== trackInfo.chapterTitle) {
3524
+ playback.chapterTitle = trackInfo.chapterTitle
3525
+ changedFields.add('chapterTitle')
3526
+ changed = true
3527
+ }
3528
+ }
3529
+ }
3530
+
3531
+ if (playback.chapterKey) {
3532
+ const chapterInfo = entry.chaptersByKey.get(playback.chapterKey)
3533
+ if (chapterInfo && playback.chapterTitle !== chapterInfo.title) {
3534
+ playback.chapterTitle = chapterInfo.title
3535
+ changedFields.add('chapterTitle')
3536
+ changed = true
3537
+ }
3538
+ }
3539
+
3540
+ return changed
3541
+ }
3542
+
3335
3543
  /**
3336
3544
  * Create an empty device config object
3337
3545
  * @returns {YotoDeviceModelConfig}
@@ -1,4 +1,5 @@
1
1
  /** @import { YotoDevice } from './api-endpoints/devices.js' */
2
+ /** @import { YotoPlaybackState } from './yoto-device.js' */
2
3
 
3
4
  import test from 'node:test'
4
5
  import assert from 'node:assert/strict'
@@ -15,6 +16,122 @@ import {
15
16
  } from './test-helpers/device-model-test-helpers.js'
16
17
 
17
18
  const envPath = join(import.meta.dirname, '..', '.env')
19
+ const PLAYBACK_SAMPLE_SCAN_LIMIT = 5
20
+
21
+ /**
22
+ * @typedef {Object} PlaybackSample
23
+ * @property {string} cardId
24
+ * @property {Map<string, { title: string, duration: number, chapterKey: string, chapterTitle: string }>} tracksByKey
25
+ * @property {string} chapterKey
26
+ * @property {string} chapterTitle
27
+ * @property {string} trackKey
28
+ * @property {string} trackTitle
29
+ * @property {number} trackLength
30
+ */
31
+
32
+ /**
33
+ * @param {YotoDeviceModel} model
34
+ * @param {(playback: YotoPlaybackState, changedFields: Set<keyof YotoPlaybackState>) => boolean} predicate
35
+ * @param {number} timeoutMs
36
+ * @param {string} label
37
+ * @returns {Promise<[YotoPlaybackState, Set<keyof YotoPlaybackState>]>}
38
+ */
39
+ function waitForPlaybackUpdateMatching (model, predicate, timeoutMs, label) {
40
+ return new Promise((resolve, reject) => {
41
+ const timeout = setTimeout(() => {
42
+ model.off('playbackUpdate', handler)
43
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`))
44
+ }, timeoutMs)
45
+
46
+ /**
47
+ * @param {YotoPlaybackState} playback
48
+ * @param {Set<keyof YotoPlaybackState>} changedFields
49
+ */
50
+ const handler = (playback, changedFields) => {
51
+ let matches = false
52
+
53
+ try {
54
+ matches = predicate(playback, changedFields)
55
+ } catch (err) {
56
+ clearTimeout(timeout)
57
+ model.off('playbackUpdate', handler)
58
+ reject(err)
59
+ return
60
+ }
61
+
62
+ if (!matches) {
63
+ return
64
+ }
65
+
66
+ clearTimeout(timeout)
67
+ model.off('playbackUpdate', handler)
68
+ resolve([playback, changedFields])
69
+ }
70
+
71
+ model.on('playbackUpdate', handler)
72
+ })
73
+ }
74
+
75
+ /**
76
+ * @param {YotoClient} client
77
+ * @returns {Promise<PlaybackSample | null>}
78
+ */
79
+ async function findPlaybackSample (client) {
80
+ const response = await client.getUserMyoContent({ showDeleted: false })
81
+
82
+ for (const card of response.cards.slice(0, PLAYBACK_SAMPLE_SCAN_LIMIT)) {
83
+ if (!card.cardId || card.deleted) {
84
+ continue
85
+ }
86
+
87
+ try {
88
+ const contentResponse = await client.getContent({ cardId: card.cardId })
89
+ const chapters = contentResponse.card.content.chapters
90
+ const firstChapter = chapters.find(candidate => candidate.tracks.length > 0)
91
+
92
+ if (!firstChapter) {
93
+ continue
94
+ }
95
+
96
+ const firstTrack = firstChapter.tracks[0]
97
+
98
+ if (!firstTrack || typeof firstTrack.title !== 'string' || !Number.isFinite(firstTrack.duration)) {
99
+ continue
100
+ }
101
+
102
+ const tracksByKey = new Map()
103
+
104
+ for (const chapter of chapters) {
105
+ for (const track of chapter.tracks) {
106
+ if (!track || typeof track.title !== 'string' || !Number.isFinite(track.duration)) {
107
+ continue
108
+ }
109
+
110
+ tracksByKey.set(track.key, {
111
+ title: track.title,
112
+ duration: track.duration,
113
+ chapterKey: chapter.key,
114
+ chapterTitle: chapter.title
115
+ })
116
+ }
117
+ }
118
+
119
+ return {
120
+ cardId: contentResponse.card.cardId,
121
+ tracksByKey,
122
+ chapterKey: firstChapter.key,
123
+ chapterTitle: firstChapter.title,
124
+ trackKey: firstTrack.key,
125
+ trackTitle: firstTrack.title,
126
+ trackLength: firstTrack.duration
127
+ }
128
+ } catch {
129
+ continue
130
+ }
131
+ }
132
+
133
+ return null
134
+ }
18
135
 
19
136
  /**
20
137
  * @returns {YotoClient}
@@ -86,3 +203,104 @@ test('YotoDeviceModel - online devices', async (t) => {
86
203
  await assertDeviceModel(client, onlineV3)
87
204
  })
88
205
  })
206
+
207
+ test('YotoDeviceModel - playback normalization', async (t) => {
208
+ loadTestTokens()
209
+ const client = createTestClient()
210
+ const response = await client.getDevices()
211
+
212
+ const onlineDevice = response.devices.find(device => device.online)
213
+ if (!onlineDevice) {
214
+ t.skip('No online device found')
215
+ return
216
+ }
217
+
218
+ const sample = await findPlaybackSample(client)
219
+ if (!sample) {
220
+ t.skip('No MYO content with tracks available')
221
+ return
222
+ }
223
+
224
+ const model = new YotoDeviceModel(client, onlineDevice)
225
+
226
+ try {
227
+ await waitForModelReady(model)
228
+ assertPlaybackShape(model.playback)
229
+
230
+ const mqttClient = model.mqttClient
231
+ assert.ok(mqttClient, 'MQTT client should be initialized')
232
+
233
+ await t.test('card cache enrichment', async () => {
234
+ if (model.playback.cardId === sample.cardId) {
235
+ const resetPromise = waitForPlaybackUpdateMatching(
236
+ model,
237
+ (playback, changedFields) => playback.cardId === null && changedFields.has('cardId'),
238
+ 5000,
239
+ 'playback card reset'
240
+ )
241
+
242
+ mqttClient.emit('events', 'test', { cardId: 'none' })
243
+ await resetPromise
244
+ }
245
+
246
+ const cardUpdatePromise = waitForPlaybackUpdateMatching(
247
+ model,
248
+ (playback, changedFields) => playback.cardId === sample.cardId && changedFields.has('cardId'),
249
+ 5000,
250
+ 'playback card update'
251
+ )
252
+
253
+ const cacheUpdatePromise = waitForPlaybackUpdateMatching(
254
+ model,
255
+ (playback, changedFields) => {
256
+ if (playback.cardId !== sample.cardId) {
257
+ return false
258
+ }
259
+
260
+ return (
261
+ changedFields.has('trackTitle') ||
262
+ changedFields.has('trackLength') ||
263
+ changedFields.has('chapterKey') ||
264
+ changedFields.has('chapterTitle')
265
+ )
266
+ },
267
+ 15000,
268
+ 'playback cache update'
269
+ )
270
+
271
+ mqttClient.emit('events', 'test', {
272
+ cardId: sample.cardId,
273
+ trackKey: sample.trackKey
274
+ })
275
+
276
+ const [cardPlayback] = await cardUpdatePromise
277
+ assert.equal(cardPlayback.cardId, sample.cardId)
278
+
279
+ const [cachePlayback] = await cacheUpdatePromise
280
+ assert.ok(cachePlayback.trackKey, 'trackKey should be set after cache update')
281
+ const trackInfo = sample.tracksByKey.get(cachePlayback.trackKey)
282
+ assert.ok(trackInfo, 'trackKey should exist in card content')
283
+ assert.equal(cachePlayback.trackTitle, trackInfo.title)
284
+ assert.equal(cachePlayback.trackLength, trackInfo.duration)
285
+ assert.equal(cachePlayback.chapterKey, trackInfo.chapterKey)
286
+ assert.equal(cachePlayback.chapterTitle, trackInfo.chapterTitle)
287
+ })
288
+
289
+ await t.test('cardId none normalization', async () => {
290
+ const noneUpdatePromise = waitForPlaybackUpdateMatching(
291
+ model,
292
+ (playback, changedFields) => playback.cardId === null && changedFields.has('cardId'),
293
+ 5000,
294
+ 'playback cardId none normalization'
295
+ )
296
+
297
+ mqttClient.emit('events', 'test', { cardId: 'none' })
298
+
299
+ const [nonePlayback, changedFields] = await noneUpdatePromise
300
+ assert.equal(nonePlayback.cardId, null)
301
+ assert.ok(changedFields.has('cardId'), 'cardId should be in changed fields')
302
+ })
303
+ } finally {
304
+ await model.stop()
305
+ }
306
+ })
package/package.json CHANGED
@@ -1,19 +1,21 @@
1
1
  {
2
2
  "name": "yoto-nodejs-client",
3
3
  "description": "(Unofficial) Node.js client for the Yoto API with automatic token refresh, MQTT device communication, and TypeScript support",
4
- "version": "0.0.8",
4
+ "version": "0.0.9",
5
5
  "author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/bcomnes/yoto-nodejs-client/issues"
8
8
  },
9
9
  "dependencies": {
10
+ "fastq": "^1.20.1",
10
11
  "jwt-decode": "^4.0.0",
11
12
  "mqtt": "^5.14.1",
13
+ "quick-lru": "^7.3.0",
12
14
  "undici": "^7.16.0"
13
15
  },
14
16
  "devDependencies": {
15
- "@unblessed/node": "1.0.0-alpha.23",
16
17
  "@types/node": "^25.0.0",
18
+ "@unblessed/node": "1.0.0-alpha.23",
17
19
  "@voxpelli/tsconfig": "^16.1.0",
18
20
  "argsclopts": "^1.0.5",
19
21
  "auto-changelog": "^2.0.0",
@@ -69,9 +71,17 @@
69
71
  "type": "git",
70
72
  "url": "https://github.com/bcomnes/yoto-nodejs-client.git"
71
73
  },
74
+ "funding": {
75
+ "type": "individual",
76
+ "url": "https://github.com/sponsors/bcomnes"
77
+ },
78
+ "c8": {
79
+ "reporter": [
80
+ "lcov",
81
+ "text"
82
+ ]
83
+ },
72
84
  "scripts": {
73
- "prepublishOnly": "npm run build && git push --follow-tags && gh-release -y",
74
- "postpublish": "npm run clean",
75
85
  "test": "run-s test:*",
76
86
  "test:lint": "eslint",
77
87
  "test:tsc": "tsc",
@@ -85,15 +95,5 @@
85
95
  "clean:declarations-top": "rm -rf $(find . -maxdepth 1 -type f -name '*.d.ts*' -o -name '*.d.cts*' -o -name '*.d.mts*')",
86
96
  "clean:declarations-lib": "rm -rf $(find lib -type f \\( -name '*.d.ts*' -o -name '*.d.cts*' -o -name '*.d.mts*' \\) ! -name '*-types.d.ts')",
87
97
  "clean:declarations-bin": "rm -rf $(find bin -type f \\( -name '*.d.ts*' -o -name '*.d.cts*' -o -name '*.d.mts*' \\) ! -name '*-types.d.ts')"
88
- },
89
- "funding": {
90
- "type": "individual",
91
- "url": "https://github.com/sponsors/bcomnes"
92
- },
93
- "c8": {
94
- "reporter": [
95
- "lcov",
96
- "text"
97
- ]
98
98
  }
99
- }
99
+ }