import { open } from 'node:fs/promises' import { releaseNotesAcknowledgeSchema, releaseNotesFileSchema, type ReleaseNote, type ReleaseNotesSnapshot } from '../shared/release-notes-contracts' import type { ApplicationSettingsStore } from './application-settings-store' import { compareStrictSemVer } from './version-checker' const maximumReleaseNotesBytes = 1024 * 1024 export class ReleaseNotesService { private releases?: ReleaseNote[] private releaseLoad?: Promise constructor( private readonly dependencies: { currentVersion: string filePath: string settingsStore: ApplicationSettingsStore } ) {} private async loadReleases(): Promise { if (this.releases) { return this.releases } if (!this.releaseLoad) { this.releaseLoad = this.readReleases().finally(() => { this.releaseLoad = undefined }) } return this.releaseLoad } private async readReleases(): Promise { const handle = await open(this.dependencies.filePath, 'r') try { const buffer = Buffer.alloc(maximumReleaseNotesBytes + 1) const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0) if (bytesRead > maximumReleaseNotesBytes) { throw new Error('Release notes exceed the size limit') } const parsed = releaseNotesFileSchema.parse( JSON.parse(buffer.toString('utf8', 0, bytesRead)) as unknown ) this.releases = [...parsed.releases].sort((left, right) => compareStrictSemVer(right.version, left.version) ) return this.releases } finally { await handle.close() } } async getPending(): Promise { const releases = await this.loadReleases() const currentVersion = this.dependencies.currentVersion if (!releases.some((release) => release.version === currentVersion)) { return { currentVersion, releases: [] } } const lastSeenVersion = await this.dependencies.settingsStore.getLastSeenReleaseNotesVersion() const pending = releases.filter((release) => { const comparedWithCurrent = compareStrictSemVer( release.version, currentVersion ) if (comparedWithCurrent > 0) { return false } return lastSeenVersion ? compareStrictSemVer(release.version, lastSeenVersion) > 0 : comparedWithCurrent === 0 }) return { currentVersion, releases: pending } } async acknowledge(input: unknown): Promise { const { version } = releaseNotesAcknowledgeSchema.parse(input) if (version !== this.dependencies.currentVersion) { throw new Error('Only the current release notes can be acknowledged') } const releases = await this.loadReleases() if (!releases.some((release) => release.version === version)) { throw new Error('Current release notes are unavailable') } await this.dependencies.settingsStore.setLastSeenReleaseNotesVersion( version ) } }