Skip to content
File

Blob: worker/src/shared/contracts/track.ts

typescript45 lines
1import { z } from 'zod';
2 
3const tagSchema = z
4 .string()
5 .max(256)
6 .refine((value) => new TextEncoder().encode(value).byteLength <= 256 && !/\p{Cc}/u.test(value));
7export const trackSchema = z
8 .object({
9 title: tagSchema.refine((value) => value.trim().length > 0),
10 artist: tagSchema,
11 durationMs: z.number().int().positive().max(600000).multipleOf(20),
12 })
13 .strict();
14 
15export type Track = z.infer<typeof trackSchema>;
16 
17export const nowPlayingSchema = z
18 .object({
19 revision: z.number().int().min(0).max(0xffff_ffff),
20 trackIndex: z.number().int().min(0).max(31),
21 trackCount: z.number().int().min(1).max(32),
22 track: trackSchema.nullable(),
23 })
24 .strict()
25 .refine((value) => value.trackIndex < value.trackCount);
26 
27export type NowPlaying = z.infer<typeof nowPlayingSchema>;
28 
29/** Wrapping sequence comparison within one publisher generation. */
30export function newerRevision(next: number, previous: number): boolean {
31 const distance = (next - previous) >>> 0;
32 return distance > 0 && distance < 0x8000_0000;
33}
34 
35export function sameNowPlaying(a: NowPlaying, b: NowPlaying): boolean {
36 return (
37 a.revision === b.revision &&
38 a.trackIndex === b.trackIndex &&
39 a.trackCount === b.trackCount &&
40 a.track?.title === b.track?.title &&
41 a.track?.artist === b.track?.artist &&
42 a.track?.durationMs === b.track?.durationMs
43 );
44}