diff --git a/docs/ADR-INDEX.md b/docs/ADR-INDEX.md index fbdf516b..a54c6cb3 100644 --- a/docs/ADR-INDEX.md +++ b/docs/ADR-INDEX.md @@ -3,7 +3,7 @@ Generated by `scripts/adr_index.py` from the records in `docs/decisions/` — do not edit; run `make adr-index` after adding or changing one, and CI fails on a stale copy (ADR-0127 point 6). -139 current records (139 accepted, +140 current records (140 accepted, 0 proposed), 10 retired. A record whose status carries a note has had a point superseded by a later one; the note says which. ## Apple clients @@ -180,6 +180,7 @@ Generated by `scripts/adr_index.py` from the records in `docs/decisions/` — do | [0138](decisions/ADR-0138-a-desktop-server-yields-to-its-owner.md) | A Desktop Server Yields to Its Owner | accepted | | [0142](decisions/ADR-0142-the-server-watches-its-library.md) | The Server Watches Its Library | accepted | | [0147](decisions/ADR-0147-windows-and-android-are-planned.md) | Windows and Android Are Planned | accepted | +| [0150](decisions/ADR-0150-each-server-and-listener-gets-8-gb-on-a-device.md) | Each Server and Listener Gets 8 GB on a Device | accepted | ## Security diff --git a/docs/decisions/ADR-0137-the-phone-keeps-a-copy-of-its-library.md b/docs/decisions/ADR-0137-the-phone-keeps-a-copy-of-its-library.md index df9e707a..e3bf92de 100644 --- a/docs/decisions/ADR-0137-the-phone-keeps-a-copy-of-its-library.md +++ b/docs/decisions/ADR-0137-the-phone-keeps-a-copy-of-its-library.md @@ -1,6 +1,6 @@ # ADR-0137: The Phone Keeps a Copy of Its Library -Status: accepted +Status: accepted; point 5 superseded by [ADR-0150](ADR-0150-each-server-and-listener-gets-8-gb-on-a-device.md) Date: 2026-09-29 @@ -11,12 +11,31 @@ Implementation: 1. **The away state (point 4)** and the reachability it depends on: "can this phone see its server", which is not `Connectivity.isOnline`. Point 3's triggers and point 4's dimming both need it. Offline favourite toggles join the event queue here. - 2. **The storage budget (point 5).** It comes before point 1 so that turning the default on can - never fill a phone without a bound. + 2. **The storage budget (point 5, now ADR-0150).** It comes before point 1 so that turning the + default on can never fill a phone without a bound. ADR-0150 replaced "a share of free space" + with 8 GB per server and profile, which needs storage split per pair first. 3. **The pairing offer (point 1).** 4. **Kept playlists and the sync that follows them (points 2 and 3).** 5. **The Mac against a server on the same machine (point 6).** It depends on nothing above, so it can move earlier. +- **Slice 1 built 2026-10-04** in `familiar-apple`, not yet run on a device: + - #210: reachability from the contract check, and the away state in the player and rows. + - #211: "Needs " wherever the server is needed. + - #212: favourites kept on disk, with changes made away queued as absolute adds and removes. + **There had been no favourites cache at all**, so a phone launched away from its server + showed no favourites. Point 4 assumed browsing worked from cache everywhere. +- **Slice 2 built** as ADR-0150 (#213–#216). +- **Slices 3–5 built 2026-10-04**, not yet run on a device: + - #217: point 1. The toggle is above the profile list, because choosing a profile saves at once. + - #218: point 6. Same machine is decided by address, loopback or one of this Mac's own, which + covers both ways a same-Mac server is paired without browsing. The play cache is off too. + - #219: points 2 and 3. `KeptSet` and one `syncKeptTracks()` replace the favourites-only + auto-download. **A sync that cannot read every rule releases nothing**, so a network blip is + never why music leaves the phone. Un-favouriting a kept favourite now releases it, which it + did not before. +- **Follow-up:** point 3's opportunistic background refresh is not built. It needs a + `BGTaskScheduler` identifier and background modes in `familiar-apple`'s Info.plist. Foreground + and the server's return cover the cases the point names. Extends [ADR-0131](ADR-0131-the-server-is-its-own-app.md), [ADR-0009](ADR-0009-offline-downloads-are-background-transfers.md), diff --git a/docs/decisions/ADR-0150-each-server-and-listener-gets-8-gb-on-a-device.md b/docs/decisions/ADR-0150-each-server-and-listener-gets-8-gb-on-a-device.md new file mode 100644 index 00000000..b9f75de3 --- /dev/null +++ b/docs/decisions/ADR-0150-each-server-and-listener-gets-8-gb-on-a-device.md @@ -0,0 +1,128 @@ +# ADR-0150: Each Server and Listener Gets 8 GB on a Device + +Status: accepted + +Date: 2026-10-04 + +Implementation: +- Accepted 2026-10-04. Built in `familiar-apple` in three parts, in this order: the per-pair + storage partition (points 1, 3 and 6), the budget (point 2), then the warning and the storage + screen (points 4 and 5). +- **Built 2026-10-04, all six points, as a stack in `familiar-apple`.** None of it had been run on + a device when this was written. + - #213: points 1 and 6. `StorageSpaces` hands out one `DownloadStore` per pair. A download + request is stamped with its pair, which travels in the task description, so a transfer that + finishes after a switch of server still lands with whoever asked. The shared folders are + renamed into the pair in use, and never merged into a pair folder that already exists. + - #214: point 3. The `Kept` directory and `DownloadRequest.residency`. Downloading a kept track + moves it into `Downloads`. + - #215: point 2. `StorageBudget`. The play cache's bound is 8 GB on the Mac too. **Auto-download + queues only what the room left by kept tracks can hold**, which this ADR did not spell out: + without it, the budget releases a favourite and the next launch fetches it straight back. + - #216: points 4 and 5. `ServerConfiguration.storageNotice`, and "Storage on this device" in + Settings → Downloads. Offering to remove files at the moment a server is forgotten is not + built: forgotten pairs stay listed there instead. + +Supersedes point 5 of [ADR-0137](ADR-0137-the-phone-keeps-a-copy-of-its-library.md). +Extends [ADR-0010](ADR-0010-played-bytes-are-cached-downloads-are-pinned.md). + +## Context + +ADR-0137 point 5 gave kept tracks a budget "defaulting to a share of free space the owner can +change". The owner has since set the rule directly: **8 GB per server and profile, and say so when +a device starts holding more than one.** + +Checked against `familiar-apple` on 2026-10-04, these facts shape the change: + +- **Storage is global today.** `DownloadStore` keeps one directory, `Application + Support/Familiar/Downloads` (`FamiliarKit/DownloadStore.swift`). `PlayCacheStore` keeps another, + `Application Support/Familiar/Cache` (`FamiliarKit/PlayCacheStore.swift`). Every server and profile + this device has used shares both. Track ids are per server, so a second server's files sit + in the same folders as the first's, and nothing can free one server's share without the other's. +- **The play cache already has a budget**, from ADR-0010's measurement: 16 GB on the Mac and 8 GB on + the phone (`PlayCacheStore.defaultBudgetBytes`). That budget is per device, not per server. +- **A device holds up to five server/profile pairs.** `RecentServers.limit` is 5, and + each entry is one server with one profile. Five pairs at 8 GB each is 40 GB. +- **A download does not record why it was made.** `DownloadedTrack` carries an id, a file name, + a size, tags and a completion time. So the favourites already fetched by auto-download + (`FavoritesAutoDownload`, ADR-0029 point 4) cannot be told apart from tracks the listener + downloaded by hand. ADR-0137 point 5 distinguishes the two ("auto-kept" against "downloaded + explicitly"), and nothing on disk does. + +## Decision + +1. **Each server-and-profile pair on a device has its own storage.** Downloads, kept tracks and the + play cache live under one folder per pair, keyed by the server's `server_id` (ADR-0134 point 4) + or, for a server reached by typed address, its host and port, and then by profile id. Switching + pair switches folder. A track downloaded for one listener is not visible to another. + +2. **The budget is 8 GB per pair, on the phone and the Mac alike.** It covers two classes of file: + - **kept** tracks, fetched because a rule said so: favourites auto-download, and kept + playlists (ADR-0137 point 2); + - **cached** tracks, kept because they were played (ADR-0010). + + When the two together would pass 8 GB, cached tracks are released first, least recently played + first, then kept tracks in the same order. **Downloaded** tracks, the ones the listener asked + for, are outside the budget and are never released automatically. That is ADR-0009 point 10's + promise, unchanged. + +3. **Kept is a third class with its own directory**, beside ADR-0010's pinned `Downloads` and cached + `Cache`. ADR-0010 point 2 made a file's class its directory so reconciliation can repair a + mislabel, and a rule-fetched file is a different class from a chosen one. Downloading a kept + track explicitly promotes it into `Downloads` with a rename, as ADR-0010 point 6 does for a + cached one. + +4. **The device says so when it starts holding a second pair.** Connecting this device to a pair + it has not held before, by pairing, by setup or from the recent list, shows the cost before + anything is saved: "Each server and listener on this iPhone keeps up to 8 GB. With 3, Familiar + may use up to 24 GB." The count is the pairs on the recent list plus the new one. It is shown + once per new pair, never when switching between pairs the device already holds, and never for + the first one. + +5. **Storage on the device is shown per pair**, in Settings → Downloads: what each pair holds, + split into downloaded, kept and cached, with a way to remove one pair's files. Forgetting a + pair on the recent list offers to remove its files too, since nothing else can reach them. + +6. **What is on disk at upgrade belongs to the pair in use then.** The global `Downloads` and + `Cache` move into that pair's folder. Files from other servers cannot be attributed, because a + track id says nothing about its server, and reconciliation against that pair's catalogue later + releases the cached ones. Existing downloads all stay **downloaded**, auto-downloaded favourites + included, because nothing records which they were. A listener who wants them counted against + the budget can remove and re-keep them. + +## Alternatives Considered + +- **A share of free space, as ADR-0137 point 5 first said.** It adapts to the device without a + number anyone has to pick. Rejected by the owner: the share moves as the phone fills with other + things, so nobody can predict what Familiar will take, and it hides the cost of a second server + inside a number that was already changing. +- **One budget for the device, shared by every pair.** No partitioning, and the total can never + pass 8 GB. Rejected: the pair in use would evict the others' files every time it played, so + switching back to a server would find its favourites gone. It also leaves the global folders, + where removing one server's files is impossible. +- **Separate budgets for kept and cached tracks**, the play cache keeping ADR-0010's figure. Each + class would get its own measured size. Rejected by the owner in favour of one number per pair a + listener can hold in their head. That would mean up to 16 GB per pair, twice what anyone was + told. +- **Keep the Mac at 16 GB**, ADR-0010's measured figure. It holds a third of plays against roughly + a quarter at 8 GB. Rejected for one rule on every device. ADR-0137 point 6 already removes the + Mac's largest case, a server on the same machine, where the Mac downloads nothing. +- **Count explicit downloads against the budget too.** One number would bound everything. Rejected: + a full budget would then have to refuse a download or release one the listener chose. Both break + ADR-0009 point 10, which promises that nothing asked for is removed. + +## Consequences + +- **Positive:** what Familiar can take is a number a listener can work out before agreeing to it, + and the cost of every further server or listener is stated at the moment it is incurred. +- **Positive:** a server's files can be removed without touching another's, and forgetting a + server can free its space. +- **Tradeoff:** on the Mac, the play cache shrinks from 16 GB to at most 8 GB, shared with kept + tracks. ADR-0010's table puts 8 GB of cache alone at a 23.6% hit rate against 33.3% at 16 GB. + Shared with kept tracks it will be lower. The cost falls on a Mac playing from a NAS. +- **Tradeoff:** two listeners on one device who both download a track hold it twice. +- **Tradeoff:** favourites auto-downloaded before this ships stay outside the budget (point 6) + until removed and re-kept. +- **Follow-up:** the storage partition (points 1, 3 and 6) is the prerequisite for everything else + here and for ADR-0137's slices 3 and 4. It lands first, then the budget, then the alert and the + storage screen.