api memory regression fix

This commit is contained in:
Boof2015
2026-03-29 19:54:38 -04:00
parent 6f73802252
commit c75faf6f30
2 changed files with 253 additions and 32 deletions
+66 -32
View File
@@ -43,8 +43,27 @@ interface AstraIntegrationServiceOptions {
clearTimeoutImpl?: typeof clearTimeout
}
function cloneTrackSnapshot(track: AstraTrackSnapshot | null): AstraTrackSnapshot | null {
if (!track) return null
return { ...track }
}
function cloneSnapshot(snapshot: AstraNowPlayingSnapshot | null): AstraNowPlayingSnapshot | null {
if (!snapshot) return null
return {
...snapshot,
currentTrack: cloneTrackSnapshot(snapshot.currentTrack),
}
}
function cloneState(state: AstraIntegrationState): AstraIntegrationState {
return JSON.parse(JSON.stringify(state)) as AstraIntegrationState
return {
config: { ...state.config },
connectionState: state.connectionState,
lastError: state.lastError,
lastControlError: state.lastControlError,
snapshot: cloneSnapshot(state.snapshot),
}
}
function getErrorMessage(error: unknown, fallback: string): string {
@@ -76,6 +95,11 @@ function bufferToDataUrl(bytes: Buffer, mimeType: string): string {
return `data:${mimeType};base64,${bytes.toString('base64')}`
}
function getArtworkKey(trackId: string | null, artworkUrl: string | null): string | null {
if (!trackId || !artworkUrl) return null
return `${trackId}\n${artworkUrl}`
}
function normalizeBaseUrl(value: unknown): string {
if (typeof value !== 'string') return DEFAULT_ASTRA_BASE_URL
const trimmed = value.trim()
@@ -215,8 +239,9 @@ export class AstraIntegrationService {
private state = createDefaultState()
private remoteSnapshot: RemoteNowPlayingSnapshot | null = null
private currentArtworkTrackId: string | null = null
private currentArtworkKey: string | null = null
private currentArtworkDataUrl: string | null = null
private failedArtworkKey: string | null = null
private streamAbortController: AbortController | null = null
private reconnectTimer: TimerHandle | null = null
private reconnectAttempt = 0
@@ -362,8 +387,9 @@ export class AstraIntegrationService {
if (!this.isScopeActive() || this.disposed) {
this.remoteSnapshot = null
this.currentArtworkTrackId = null
this.currentArtworkKey = null
this.currentArtworkDataUrl = null
this.failedArtworkKey = null
this.state = {
...this.state,
connectionState: 'disabled',
@@ -390,6 +416,7 @@ export class AstraIntegrationService {
connectionState: 'connecting',
lastError: null,
}
this.failedArtworkKey = null
this.emitState()
try {
@@ -577,7 +604,10 @@ export class AstraIntegrationService {
private applyRemoteSnapshot(snapshot: RemoteNowPlayingSnapshot): void {
this.remoteSnapshot = snapshot
const artworkDataUrl = snapshot.currentTrack?.id === this.currentArtworkTrackId
const artworkDataUrl = getArtworkKey(
snapshot.currentTrack?.id ?? null,
snapshot.currentTrack?.artworkUrl ?? null,
) === this.currentArtworkKey
? this.currentArtworkDataUrl
: null
@@ -589,42 +619,41 @@ export class AstraIntegrationService {
}
private async refreshArtwork(trackId: string | null, artworkUrl: string | null): Promise<void> {
if (!trackId || !artworkUrl || !this.remoteSnapshot?.currentTrack || this.remoteSnapshot.currentTrack.id !== trackId) {
this.currentArtworkTrackId = trackId
const artworkKey = getArtworkKey(trackId, artworkUrl)
const remoteArtworkKey = getArtworkKey(
this.remoteSnapshot?.currentTrack?.id ?? null,
this.remoteSnapshot?.currentTrack?.artworkUrl ?? null,
)
if (!artworkKey || remoteArtworkKey !== artworkKey) {
this.currentArtworkKey = null
this.currentArtworkDataUrl = null
if (this.remoteSnapshot) {
this.state = {
...this.state,
snapshot: toRendererSnapshot(this.remoteSnapshot, null),
}
this.emitState()
}
this.failedArtworkKey = null
return
}
if (this.currentArtworkTrackId === trackId && this.currentArtworkDataUrl) {
this.state = {
...this.state,
snapshot: toRendererSnapshot(this.remoteSnapshot, this.currentArtworkDataUrl),
}
this.emitState()
if (this.currentArtworkKey === artworkKey && this.currentArtworkDataUrl) {
return
}
const response = await this.fetchImpl(artworkUrl, {
if (this.failedArtworkKey === artworkKey) {
return
}
this.currentArtworkKey = null
this.currentArtworkDataUrl = null
if (artworkUrl === null) {
return
}
const resolvedArtworkUrl = artworkUrl
const response = await this.fetchImpl(resolvedArtworkUrl, {
headers: this.buildAuthHeaders(),
}).catch(() => null)
if (!response || !response.ok) {
this.currentArtworkTrackId = trackId
this.currentArtworkDataUrl = null
if (this.remoteSnapshot?.currentTrack?.id === trackId) {
this.state = {
...this.state,
snapshot: toRendererSnapshot(this.remoteSnapshot, null),
}
this.emitState()
}
this.failedArtworkKey = artworkKey
return
}
@@ -632,15 +661,20 @@ export class AstraIntegrationService {
const bytes = Buffer.from(await response.arrayBuffer())
const artworkDataUrl = bufferToDataUrl(bytes, mimeType)
if (this.remoteSnapshot?.currentTrack?.id !== trackId) {
const remoteSnapshot = this.remoteSnapshot
if (!remoteSnapshot || getArtworkKey(
remoteSnapshot.currentTrack?.id ?? null,
remoteSnapshot.currentTrack?.artworkUrl ?? null,
) !== artworkKey) {
return
}
this.currentArtworkTrackId = trackId
this.currentArtworkKey = artworkKey
this.currentArtworkDataUrl = artworkDataUrl
this.failedArtworkKey = null
this.state = {
...this.state,
snapshot: toRendererSnapshot(this.remoteSnapshot, artworkDataUrl),
snapshot: toRendererSnapshot(remoteSnapshot, artworkDataUrl),
}
this.emitState()
}
+187
View File
@@ -232,6 +232,193 @@ test('service initializes from config, hydrates artwork, and applies SSE updates
}
})
test('service emits a single state update when reusing cached artwork on SSE updates', async () => {
const harness = await createConfigFile({
baseUrl: DEFAULT_ASTRA_BASE_URL,
token: 'secret-token',
})
const sse = createSseStream()
const artworkUrl = `${DEFAULT_ASTRA_BASE_URL}/v1/artwork/current?trackId=track-1`
let stateUpdateCount = 0
const fetchImpl: typeof fetch = async (input, init) => {
const url = String(input)
const pathname = new URL(url).pathname
const headers = createHeaders(init)
assert.equal(headers.get('authorization'), 'Bearer secret-token')
if (pathname === '/v1/now-playing') {
return createJsonResponse({
playbackState: 'playing',
currentTime: 15,
duration: 180,
queueLength: 2,
outputDeviceLabel: 'Built-in Output',
visualizerLineColor: '#4ade80',
updatedAt: 1000,
currentTrack: {
id: 'track-1',
title: 'Song One',
artist: 'Artist One',
album: 'Album One',
isFavorite: false,
artworkUrl,
},
})
}
if (pathname === '/v1/artwork/current') {
return createPngResponse('cover-one')
}
if (pathname === '/v1/events') {
return sse.response
}
throw new Error(`Unexpected request: ${url}`)
}
const service = new AstraIntegrationService({
configPath: harness.configPath,
fetchImpl,
now: () => 1000,
})
try {
service.subscribe((state) => {
void state
stateUpdateCount += 1
})
await service.initialize()
await service.setConsumerActive(1, true)
await waitFor(() => service.getState().connectionState === 'connected', 'expected connected Astra state')
const baseUpdateCount = stateUpdateCount
sse.pushEvent('now-playing', {
playbackState: 'playing',
currentTime: 42,
duration: 180,
queueLength: 2,
outputDeviceLabel: 'Built-in Output',
visualizerLineColor: '#4ade80',
updatedAt: 2000,
currentTrack: {
id: 'track-1',
title: 'Song One',
artist: 'Artist One',
album: 'Album One',
isFavorite: false,
artworkUrl,
},
})
await waitFor(() => service.getState().snapshot?.updatedAt === 2000, 'expected SSE update to apply')
assert.equal(stateUpdateCount - baseUpdateCount, 1)
} finally {
await service.dispose()
await harness.cleanup()
}
})
test('service does not refetch identical artwork after a failed attempt', async () => {
const harness = await createConfigFile({
baseUrl: DEFAULT_ASTRA_BASE_URL,
token: 'secret-token',
})
const sse = createSseStream()
const artworkUrl = `${DEFAULT_ASTRA_BASE_URL}/v1/artwork/current?trackId=track-2`
let artworkRequests = 0
const fetchImpl: typeof fetch = async (input, init) => {
const url = String(input)
const pathname = new URL(url).pathname
const headers = createHeaders(init)
assert.equal(headers.get('authorization'), 'Bearer secret-token')
if (pathname === '/v1/now-playing') {
return createJsonResponse({
playbackState: 'paused',
currentTime: 0,
duration: 0,
queueLength: 0,
outputDeviceLabel: null,
visualizerLineColor: '#38bdf8',
updatedAt: 1000,
currentTrack: null,
})
}
if (pathname === '/v1/artwork/current') {
artworkRequests += 1
return createJsonResponse({ error: 'missing artwork' }, 404)
}
if (pathname === '/v1/events') {
return sse.response
}
throw new Error(`Unexpected request: ${url}`)
}
const service = new AstraIntegrationService({
configPath: harness.configPath,
fetchImpl,
})
try {
await service.initialize()
await service.setConsumerActive(1, true)
await waitFor(() => service.getState().connectionState === 'connected', 'expected connected Astra state')
sse.pushEvent('now-playing', {
playbackState: 'playing',
currentTime: 5,
duration: 180,
queueLength: 1,
outputDeviceLabel: 'Built-in Output',
visualizerLineColor: '#4ade80',
updatedAt: 2000,
currentTrack: {
id: 'track-2',
title: 'Song Two',
artist: 'Artist Two',
album: 'Album Two',
isFavorite: false,
artworkUrl,
},
})
await waitFor(() => service.getState().snapshot?.currentTrack?.id === 'track-2', 'expected first artwork-bearing track update')
await waitFor(() => artworkRequests === 1, 'expected first failed artwork request')
sse.pushEvent('now-playing', {
playbackState: 'playing',
currentTime: 15,
duration: 180,
queueLength: 1,
outputDeviceLabel: 'Built-in Output',
visualizerLineColor: '#4ade80',
updatedAt: 3000,
currentTrack: {
id: 'track-2',
title: 'Song Two',
artist: 'Artist Two',
album: 'Album Two',
isFavorite: false,
artworkUrl,
},
})
await waitFor(() => service.getState().snapshot?.updatedAt === 3000, 'expected second track update')
assert.equal(artworkRequests, 1)
} finally {
await service.dispose()
await harness.cleanup()
}
})
test('service schedules reconnect when the SSE stream closes', async () => {
const harness = await createConfigFile({
baseUrl: DEFAULT_ASTRA_BASE_URL,