@goodandready/dsh-tts 0.4.32 → 0.4.33
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/CHANGELOG.md +7 -0
- package/lib/client.js +31 -3
- package/lib/engines/manager.js +8 -1
- package/lib/updater.js +25 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.33 (2026-10-11)
|
|
4
|
+
|
|
5
|
+
- **SSE Stream Watchdog**: Added idle timeout watchdog (30s) in Web player (`lib/client-src/20-player.js`) to automatically trigger reconnect on stale/frozen EventSource connection (#206).
|
|
6
|
+
- **Engine Manager Cleanup**: Added explicit `removeEventListener('abort', ...)` in `execute` finally block for model downloads in `lib/engines/manager.js` (#206).
|
|
7
|
+
- **Updater Testing**: Added behavioral test capturing `child_process.spawn` arguments during `/api/dsh-tts/update` execution verifying no bypass of minimum release age (#206).
|
|
8
|
+
- **Client Bundle**: Rebuilt `lib/client.js` from canonical client-src fragments with idle watchdog.
|
|
9
|
+
|
|
3
10
|
## 0.4.32
|
|
4
11
|
|
|
5
12
|
### Fixed
|
package/lib/client.js
CHANGED
|
@@ -527,17 +527,36 @@ window.__ModuleLoader__.load({
|
|
|
527
527
|
return () => window.removeEventListener('dsh-voice:speaking', onSpeaking)
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
// Unified SSE Realtime Stream Connection (#158)
|
|
530
|
+
// Unified SSE Realtime Stream Connection (#158, #206)
|
|
531
531
|
let activeSse = null
|
|
532
532
|
let activeSseAttempt = 0
|
|
533
533
|
let activeSseTimer = null
|
|
534
|
+
let activeSseWatchdog = null
|
|
534
535
|
let sseDisposed = false
|
|
536
|
+
const SSE_IDLE_TIMEOUT_MS = 30000
|
|
535
537
|
|
|
536
538
|
function startSseStream() {
|
|
537
539
|
if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') return () => {}
|
|
538
540
|
sseDisposed = false
|
|
539
541
|
|
|
542
|
+
const resetWatchdog = () => {
|
|
543
|
+
if (activeSseWatchdog) clearTimeout(activeSseWatchdog)
|
|
544
|
+
if (sseDisposed) return
|
|
545
|
+
activeSseWatchdog = setTimeout(() => {
|
|
546
|
+
if (activeSse && !sseDisposed) {
|
|
547
|
+
try {
|
|
548
|
+
if (typeof activeSse.onerror === 'function') {
|
|
549
|
+
activeSse.onerror(new Event('error'))
|
|
550
|
+
} else {
|
|
551
|
+
activeSse.close()
|
|
552
|
+
}
|
|
553
|
+
} catch (_err) { /* error trigger ignore */ }
|
|
554
|
+
}
|
|
555
|
+
}, SSE_IDLE_TIMEOUT_MS)
|
|
556
|
+
}
|
|
557
|
+
|
|
540
558
|
const onItem = (e) => {
|
|
559
|
+
resetWatchdog()
|
|
541
560
|
try {
|
|
542
561
|
const item = JSON.parse(e.data)
|
|
543
562
|
if (!item || !item.id) return
|
|
@@ -556,6 +575,7 @@ window.__ModuleLoader__.load({
|
|
|
556
575
|
}
|
|
557
576
|
|
|
558
577
|
const onChime = (e) => {
|
|
578
|
+
resetWatchdog()
|
|
559
579
|
try {
|
|
560
580
|
const item = JSON.parse(e.data)
|
|
561
581
|
if (!item) return
|
|
@@ -567,6 +587,7 @@ window.__ModuleLoader__.load({
|
|
|
567
587
|
|
|
568
588
|
const connect = () => {
|
|
569
589
|
if (sseDisposed) return
|
|
590
|
+
if (activeSseWatchdog) { clearTimeout(activeSseWatchdog); activeSseWatchdog = null }
|
|
570
591
|
if (activeSse) {
|
|
571
592
|
try { activeSse.close() } catch (_err) { /* active sse already closed */ }
|
|
572
593
|
activeSse = null
|
|
@@ -574,10 +595,15 @@ window.__ModuleLoader__.load({
|
|
|
574
595
|
try {
|
|
575
596
|
const es = new EventSource('/dsh-tts/stream')
|
|
576
597
|
activeSse = es
|
|
577
|
-
es.onopen = () => {
|
|
598
|
+
es.onopen = () => {
|
|
599
|
+
activeSseAttempt = 0
|
|
600
|
+
resetWatchdog()
|
|
601
|
+
}
|
|
602
|
+
es.onmessage = () => { resetWatchdog() }
|
|
578
603
|
es.addEventListener('utterance', onItem)
|
|
579
604
|
es.addEventListener('chime', onChime)
|
|
580
605
|
es.onerror = () => {
|
|
606
|
+
if (activeSseWatchdog) { clearTimeout(activeSseWatchdog); activeSseWatchdog = null }
|
|
581
607
|
if (activeSse === es) {
|
|
582
608
|
try { es.close() } catch (_err) { /* eventsource already closed */ }
|
|
583
609
|
activeSse = null
|
|
@@ -588,6 +614,7 @@ window.__ModuleLoader__.load({
|
|
|
588
614
|
if (activeSseTimer) clearTimeout(activeSseTimer)
|
|
589
615
|
activeSseTimer = setTimeout(connect, delay)
|
|
590
616
|
}
|
|
617
|
+
resetWatchdog()
|
|
591
618
|
} catch { /* EventSource failed */ }
|
|
592
619
|
}
|
|
593
620
|
|
|
@@ -595,7 +622,8 @@ window.__ModuleLoader__.load({
|
|
|
595
622
|
|
|
596
623
|
return () => {
|
|
597
624
|
sseDisposed = true
|
|
598
|
-
if (activeSseTimer) clearTimeout(activeSseTimer)
|
|
625
|
+
if (activeSseTimer) { clearTimeout(activeSseTimer); activeSseTimer = null }
|
|
626
|
+
if (activeSseWatchdog) { clearTimeout(activeSseWatchdog); activeSseWatchdog = null }
|
|
599
627
|
if (activeSse) {
|
|
600
628
|
try { activeSse.close() } catch (_err) { /* active sse already closed on dispose */ }
|
|
601
629
|
activeSse = null
|
package/lib/engines/manager.js
CHANGED
|
@@ -197,9 +197,13 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
197
197
|
const dir = getModelPath(engineId)
|
|
198
198
|
|
|
199
199
|
const controller = new AbortController()
|
|
200
|
+
let onAbortListener = null
|
|
200
201
|
if (options.signal) {
|
|
201
202
|
if (options.signal.aborted) controller.abort()
|
|
202
|
-
else
|
|
203
|
+
else {
|
|
204
|
+
onAbortListener = () => controller.abort()
|
|
205
|
+
options.signal.addEventListener('abort', onAbortListener, { once: true })
|
|
206
|
+
}
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
const downloadState = {
|
|
@@ -311,6 +315,9 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
311
315
|
lastErrors.set(engineId, { message: sanitized, failedAt: Date.now() })
|
|
312
316
|
throw err
|
|
313
317
|
} finally {
|
|
318
|
+
if (options.signal && onAbortListener) {
|
|
319
|
+
try { options.signal.removeEventListener('abort', onAbortListener) } catch (_err) { /* listener already removed */ }
|
|
320
|
+
}
|
|
314
321
|
if (activeDownloads.get(engineId) === downloadState) {
|
|
315
322
|
activeDownloads.delete(engineId)
|
|
316
323
|
}
|
package/lib/updater.js
CHANGED
|
@@ -125,7 +125,7 @@ function runtime() {
|
|
|
125
125
|
: validProfileName(basename(profileDir))
|
|
126
126
|
? basename(profileDir)
|
|
127
127
|
: 'web'
|
|
128
|
-
const cliEntry = findDshCliEntry()
|
|
128
|
+
const cliEntry = process.env.DSH_CLI_ENTRY ?? findDshCliEntry()
|
|
129
129
|
return cliEntry === undefined ? { profileName, profileDir } : { profileName, profileDir, cliEntry }
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -170,7 +170,10 @@ export function isNewerVersion(currentValue, candidateValue) {
|
|
|
170
170
|
return comparePrerelease(candidate.prerelease, current.prerelease) > 0
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
async function latestVersion(packageName, registry) {
|
|
173
|
+
async function latestVersion(packageName, registry, options) {
|
|
174
|
+
if (typeof options?.latestVersionImpl === 'function') {
|
|
175
|
+
return await options.latestVersionImpl(packageName, registry)
|
|
176
|
+
}
|
|
174
177
|
if (
|
|
175
178
|
latestCache?.packageName === packageName &&
|
|
176
179
|
latestCache.registry === registry &&
|
|
@@ -202,7 +205,7 @@ async function currentVersion(manifestUrl) {
|
|
|
202
205
|
|
|
203
206
|
export async function status(options, target) {
|
|
204
207
|
const current = await currentVersion(options.manifestUrl)
|
|
205
|
-
const latest = await latestVersion(options.packageName, options.registry ?? 'https://registry.npmjs.org')
|
|
208
|
+
const latest = await latestVersion(options.packageName, options.registry ?? 'https://registry.npmjs.org', options)
|
|
206
209
|
return {
|
|
207
210
|
packageName: options.packageName,
|
|
208
211
|
currentVersion: current,
|
|
@@ -217,7 +220,25 @@ export async function status(options, target) {
|
|
|
217
220
|
async function installExact(target, packageSpec, options) {
|
|
218
221
|
if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.')
|
|
219
222
|
await new Promise((resolvePromise, reject) => {
|
|
220
|
-
const
|
|
223
|
+
const spawnCall = options?.spawnImpl || spawn
|
|
224
|
+
const child = spawnCall === spawn ? spawn(
|
|
225
|
+
process.execPath,
|
|
226
|
+
[
|
|
227
|
+
target.cliEntry,
|
|
228
|
+
'plugin',
|
|
229
|
+
'--profile',
|
|
230
|
+
target.profileName,
|
|
231
|
+
'add',
|
|
232
|
+
packageSpec,
|
|
233
|
+
`--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
|
|
234
|
+
],
|
|
235
|
+
{
|
|
236
|
+
cwd: target.profileDir,
|
|
237
|
+
windowsHide: true,
|
|
238
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
239
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
240
|
+
}
|
|
241
|
+
) : spawnCall(
|
|
221
242
|
process.execPath,
|
|
222
243
|
[
|
|
223
244
|
target.cliEntry,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-tts",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.33",
|
|
4
4
|
"description": "Text-to-speech for DeepSeek Harness: speak agent replies in the Web UI with a provider fallback chain (OpenAI, ElevenLabs, Google, Azure, Groq, Deepgram, OpenRouter, Edge, Piper, eSpeak).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|