CI / ci (push) Failing after 4m23s
Co-authored-by: orchestrator-agent <20+orchestrator-agent@code.cubecraftcreations.com>
156 lines
5.6 KiB
TypeScript
156 lines
5.6 KiB
TypeScript
import { useSyncExternalStore } from 'react'
|
|
import { isRecord } from '../api/errors.ts'
|
|
|
|
// SAVED_KEY is the localStorage key holding, on this device, what the Supply
|
|
// run screen needs to open offline — the household's schedule and the
|
|
// Supply runs last read — and the toggles made that the server has not
|
|
// answered yet, in the order they were made.
|
|
export const SAVED_KEY = 'mealquest.supply-run'
|
|
|
|
// SAVED_WEEKS is how many weeks' Supply runs are kept: this week's and next
|
|
// week's are the only ones still current.
|
|
const SAVED_WEEKS = 2
|
|
|
|
// Schedule is the part of the household settings the Supply run screen
|
|
// needs to choose the week and say times. The service worker never caches
|
|
// the settings, so it is kept here for the screen to open offline.
|
|
export type Schedule = { timezone: string; plan_draft_dow: number; plan_draft_time: string }
|
|
|
|
// QueuedToggle is a toggle the server has not answered yet: key tells it
|
|
// apart from any other toggle of the same item.
|
|
export type QueuedToggle = { key: string; weekStart: string; id: string; needed: boolean }
|
|
|
|
// Saved is the stored value: runs holds each week's Supply run as the server
|
|
// last sent it, by its Monday.
|
|
type Saved = { schedule: Schedule | null; runs: Record<string, unknown>; queue: QueuedToggle[] }
|
|
|
|
function isSchedule(value: unknown): value is Schedule {
|
|
return (
|
|
isRecord(value) &&
|
|
typeof value.timezone === 'string' &&
|
|
Number.isInteger(value.plan_draft_dow) &&
|
|
typeof value.plan_draft_time === 'string'
|
|
)
|
|
}
|
|
|
|
function isQueuedToggle(value: unknown): value is QueuedToggle {
|
|
return (
|
|
isRecord(value) &&
|
|
typeof value.key === 'string' &&
|
|
typeof value.weekStart === 'string' &&
|
|
typeof value.id === 'string' &&
|
|
typeof value.needed === 'boolean'
|
|
)
|
|
}
|
|
|
|
// read is the stored value, empty when nothing is stored. A value that is not
|
|
// one MealQuest stored is an error.
|
|
function read(): Saved {
|
|
const text = window.localStorage.getItem(SAVED_KEY)
|
|
if (text === null) return { schedule: null, runs: {}, queue: [] }
|
|
const saved: unknown = JSON.parse(text)
|
|
if (
|
|
!isRecord(saved) ||
|
|
(saved.schedule !== null && !isSchedule(saved.schedule)) ||
|
|
!isRecord(saved.runs) ||
|
|
!Array.isArray(saved.queue) ||
|
|
!saved.queue.every(isQueuedToggle)
|
|
) {
|
|
throw new Error(`The Supply run saved on this device (${SAVED_KEY}) is not one MealQuest saved.`)
|
|
}
|
|
return { schedule: saved.schedule as Schedule | null, runs: saved.runs, queue: saved.queue }
|
|
}
|
|
|
|
function write(change: (saved: Saved) => Saved) {
|
|
window.localStorage.setItem(SAVED_KEY, JSON.stringify(change(read())))
|
|
}
|
|
|
|
// savedSchedule is the household's schedule as last read, or null.
|
|
export function savedSchedule(): Schedule | null {
|
|
return read().schedule
|
|
}
|
|
|
|
export function saveSchedule({ timezone, plan_draft_dow, plan_draft_time }: Schedule) {
|
|
write((saved) => ({ ...saved, schedule: { timezone, plan_draft_dow, plan_draft_time } }))
|
|
}
|
|
|
|
// savedRun is the week's Supply run as the server last sent it, unparsed, or
|
|
// undefined when none is saved.
|
|
export function savedRun(weekStart: string): unknown {
|
|
return read().runs[weekStart]
|
|
}
|
|
|
|
// saveRun keeps the week's Supply run as the server sent it, with the latest
|
|
// weeks'. A week with none (null) or one frozen into the Supply archive is
|
|
// not kept: nothing on it can change.
|
|
export function saveRun(weekStart: string, run: { status: string } | null) {
|
|
write((saved) => {
|
|
const runs = { ...saved.runs }
|
|
delete runs[weekStart]
|
|
if (run !== null && run.status !== 'archived') runs[weekStart] = run
|
|
const latest = Object.keys(runs).sort().slice(-SAVED_WEEKS)
|
|
return { ...saved, runs: Object.fromEntries(latest.map((week) => [week, runs[week]])) }
|
|
})
|
|
}
|
|
|
|
// queued is the toggles the server has not answered, in the order made.
|
|
export function queued(): QueuedToggle[] {
|
|
return read().queue
|
|
}
|
|
|
|
export function enqueue(toggle: QueuedToggle) {
|
|
write((saved) => ({ ...saved, queue: [...saved.queue, toggle] }))
|
|
}
|
|
|
|
export function dequeue(key: string) {
|
|
write((saved) => ({ ...saved, queue: saved.queue.filter((toggle) => toggle.key !== key) }))
|
|
}
|
|
|
|
// The keys of the toggles that will sync: made, or waiting, while the server
|
|
// could not be reached, and not settled yet. They live here, with the page,
|
|
// as the toggles in the mutation cache do: the Supply run screen is left and
|
|
// opened again while a toggle is still pending, and starts with nothing of
|
|
// its own.
|
|
const syncing = new Set<string>()
|
|
|
|
export function markWillSync(key: string) {
|
|
syncing.add(key)
|
|
}
|
|
|
|
export function willSync(key: string): boolean {
|
|
return syncing.has(key)
|
|
}
|
|
|
|
export function synced(key: string) {
|
|
syncing.delete(key)
|
|
}
|
|
|
|
// The latest failure to read or write the saved Supply run, which the Supply
|
|
// run screen shows: a toggle that cannot be kept may be lost on a reload.
|
|
let problem: Error | null = null
|
|
const listeners = new Set<() => void>()
|
|
|
|
// keep runs one read or write of the saved Supply run, and returns its
|
|
// result, or undefined when it failed: the failure is reported to the Supply
|
|
// run screen. Listeners hear of it after the current render.
|
|
export function keep<T>(action: () => T): T | undefined {
|
|
try {
|
|
return action()
|
|
} catch (error) {
|
|
problem = error instanceof Error ? error : new Error(String(error))
|
|
queueMicrotask(() => listeners.forEach((listener) => listener()))
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
function subscribe(listener: () => void) {
|
|
listeners.add(listener)
|
|
return () => listeners.delete(listener)
|
|
}
|
|
|
|
// useSavedProblem is the latest failure to read or write the saved Supply
|
|
// run, or null.
|
|
export function useSavedProblem(): Error | null {
|
|
return useSyncExternalStore(subscribe, () => problem)
|
|
}
|