ci / module boundaries / boundaries (push) Successful in 6s
ci / api / api (push) Successful in 47s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / db / db (push) Successful in 1m4s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m30s
ci / ts / ts (packages + web) (push) Successful in 2m54s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m14s
63 lines
2.1 KiB
TypeScript
63 lines
2.1 KiB
TypeScript
/** Policy shared by web and native players; queue ownership stays with the adapter. */
|
|
export const EARLY_SKIP_MS = 30_000;
|
|
export const WAVE_REFILL_AT = 2;
|
|
export type WaveOutcome = 'complete' | 'skip' | 'skip_early' | 'like' | 'dislike';
|
|
export interface WaveInfo {
|
|
sessionId: string;
|
|
activity: string | null;
|
|
mood: string | null;
|
|
fallback: boolean;
|
|
}
|
|
export interface WaveFeedback { track_id: string; outcome: WaveOutcome }
|
|
export interface WaveGateway<T> {
|
|
next(sessionId: string, feedback: WaveFeedback[]): Promise<T>;
|
|
}
|
|
|
|
export class WaveCoordinator<T> {
|
|
private sessionId: string | null = null;
|
|
private generation = 0;
|
|
private pending: WaveFeedback[] = [];
|
|
private loading: number | null = null;
|
|
|
|
constructor(private readonly gateway: WaveGateway<T>) {}
|
|
|
|
get version() { return this.generation; }
|
|
isCurrent(version: number) { return version === this.generation; }
|
|
|
|
/** Also invalidates in-flight responses when the context changes within a session. */
|
|
activate(sessionId: string | null) {
|
|
this.sessionId = sessionId;
|
|
this.pending = [];
|
|
this.loading = null;
|
|
return ++this.generation;
|
|
}
|
|
|
|
changeContext(sessionId: string) {
|
|
const pending = this.sessionId === sessionId ? this.pending : [];
|
|
const version = this.activate(sessionId);
|
|
this.pending = pending;
|
|
return version;
|
|
}
|
|
|
|
note(trackId: string, outcome: WaveOutcome) {
|
|
if (this.sessionId) this.pending.push({ track_id: trackId, outcome });
|
|
}
|
|
|
|
async refill(remaining: number, refresh: boolean, apply: (result: T, current: () => boolean) => void | Promise<void>) {
|
|
const id = this.sessionId;
|
|
const version = this.generation;
|
|
if (!id || this.loading === version || (!refresh && remaining > WAVE_REFILL_AT)) return;
|
|
this.loading = version;
|
|
const feedback = this.pending;
|
|
this.pending = [];
|
|
try {
|
|
const result = await this.gateway.next(id, feedback);
|
|
if (this.isCurrent(version)) await apply(result, () => this.isCurrent(version));
|
|
} catch {
|
|
if (this.isCurrent(version)) this.pending = [...feedback, ...this.pending];
|
|
} finally {
|
|
if (this.loading === version) this.loading = null;
|
|
}
|
|
}
|
|
}
|