-
-
Notifications
You must be signed in to change notification settings - Fork 28
Mobile: keep id-less saved TVs distinct and honest about identity (#146) #152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
59487e3
d129c7b
8207899
eff495c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,17 +11,24 @@ import type { Device, SavedDevice } from "./types"; | |
| import { deviceLabelOf } from "./types"; | ||
| import { | ||
| normalizeHardwareId, | ||
| savedDeviceIsLiveConnection, | ||
| savedDeviceKey, | ||
| savedDeviceMatchesConnection, | ||
| } from "./identity"; | ||
|
|
||
| export { savedDeviceKey, savedDeviceMatchesConnection }; | ||
| export { savedDeviceIsLiveConnection, savedDeviceKey, savedDeviceMatchesConnection }; | ||
|
|
||
| const KEY = "atv.savedDevices.v1"; | ||
| const AUTO_KEY = "atv.autoConnect.v1"; | ||
| const MAX = 16; | ||
| const EPOCH = new Date(0).toISOString(); | ||
|
|
||
| /// Not a security identifier -- just random enough that two rows saved in the | ||
| /// same process tick never collide. | ||
| function randomLocalId(): string { | ||
| return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`; | ||
| } | ||
|
|
||
| function normalizeSavedDevice(value: unknown): SavedDevice | null { | ||
| if (!value || typeof value !== "object") return null; | ||
| const d = value as Record<string, unknown>; | ||
|
|
@@ -48,10 +55,20 @@ function normalizeSavedDevice(value: unknown): SavedDevice | null { | |
| const hardwareId = normalizeHardwareId( | ||
| typeof d.hardwareId === "string" ? d.hardwareId : undefined, | ||
| ); | ||
| // Every id-less row needs a stable key of its own (see identity.ts); carry | ||
| // an existing one over, or mint one now if this row has never had one (an | ||
| // older record, or one whose only id was a placeholder that just migrated | ||
| // away above). | ||
| const localId = hardwareId | ||
| ? undefined | ||
| : typeof d.localId === "string" && d.localId.trim() !== "" | ||
| ? d.localId.trim() | ||
| : randomLocalId(); | ||
| return { | ||
| host: d.host.trim(), | ||
| connectPort: d.connectPort, | ||
| ...(hardwareId ? { hardwareId } : {}), | ||
| ...(localId ? { localId } : {}), | ||
| name: | ||
| typeof d.name === "string" && d.name.trim() !== "" | ||
| ? d.name.trim() | ||
|
|
@@ -63,6 +80,19 @@ function normalizeSavedDevice(value: unknown): SavedDevice | null { | |
| }; | ||
| } | ||
|
|
||
| /// Whether a raw stored record will come out of `normalizeSavedDevice` as | ||
| /// id-less and without an existing local key -- i.e. it is about to be | ||
| /// assigned a fresh one, which must be persisted so it stays stable. | ||
| function rawNeedsLocalId(raw: unknown): boolean { | ||
| if (!raw || typeof raw !== "object") return false; | ||
| const d = raw as Record<string, unknown>; | ||
| const hardwareId = normalizeHardwareId( | ||
| typeof d.hardwareId === "string" ? d.hardwareId : undefined, | ||
| ); | ||
| if (hardwareId) return false; | ||
| return typeof d.localId !== "string" || d.localId.trim() === ""; | ||
| } | ||
|
|
||
| function read(): SavedDevice[] { | ||
| try { | ||
| const raw = localStorage.getItem(KEY); | ||
|
|
@@ -90,7 +120,10 @@ function read(): SavedDevice[] { | |
| const stored = d.hardwareId; | ||
| return normalizeHardwareId(typeof stored === "string" ? stored : undefined) !== stored; | ||
| }); | ||
| if (unnormalizedId || unique.length !== normalized.length) write(unique); | ||
| const needsLocalIdMigration = parsed.some(rawNeedsLocalId); | ||
| if (unnormalizedId || needsLocalIdMigration || unique.length !== normalized.length) { | ||
| write(unique); | ||
| } | ||
| return unique; | ||
| } catch { | ||
| return []; | ||
|
|
@@ -165,9 +198,15 @@ export function rememberDevice( | |
| ): void { | ||
| const current = read(); | ||
| const hardwareId = hardwareIdOf(device); | ||
| const existing = current.find((d) => | ||
| sameTv(d, host, connectPort, hardwareId), | ||
| ); | ||
| const matches = current.filter((d) => sameTv(d, host, connectPort, hardwareId)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a different id-less TV is the first device to reuse an existing row's AGENTS.md reference: AGENTS.md:L45-L48 Useful? React with 👍 / 👎.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Valid, and real, but it predates this PR: the lone-match branch of |
||
| // An id-less connection can match more than one saved row only when several | ||
| // id-less TVs have shared this exact endpoint over time. Which one just | ||
| // answered is not knowable from the endpoint alone, so nothing is written: | ||
| // claiming one would be a guess, and saving a fresh row on every repeat | ||
| // reconnect would eventually evict a genuine saved TV once MAX is reached. | ||
| if (matches.length > 1) return; | ||
| const existing = matches[0]; | ||
| const combinedHardwareId = hardwareId ?? existing?.hardwareId; | ||
| const reportedFriendlyName = device?.properties?.friendly_name?.trim(); | ||
| const name = reportedFriendlyName || existing?.name || deviceLabelOf(device); | ||
| const list = current.filter((d) => d !== existing); | ||
|
|
@@ -176,9 +215,8 @@ export function rememberDevice( | |
| connectPort, | ||
| name, | ||
| deviceType: device?.device_type ?? existing?.deviceType ?? "unknown", | ||
| ...(hardwareId || existing?.hardwareId | ||
| ? { hardwareId: hardwareId ?? existing?.hardwareId } | ||
| : {}), | ||
| ...(combinedHardwareId ? { hardwareId: combinedHardwareId } : {}), | ||
| ...(combinedHardwareId ? {} : { localId: existing?.localId ?? randomLocalId() }), | ||
| lastUsed: new Date().toISOString(), | ||
| }); | ||
| write(list); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once this key allows multiple id-less rows at one endpoint to survive,
Devices.sveltelines 74–85 still filters current-device membership by calling the endpoint-basedsavedDeviceMatchesConnectionindependently for every row. If the live TV reports no serial, all distinct rows at that endpoint return true and disappear from “Other TVs,” including their Forget controls; the additional row created byrememberDevicedisappears too. That consumer needs the same uniqueness/ambiguity handling added to discovery rows so an address alone does not classify every saved identity as current.AGENTS.md reference: AGENTS.md:L45-L48
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already fixed:
otherTvsinDevices.svelteswitched to the ambiguity-awaresavedDeviceIsLiveConnectionhelper in d129c7b, so an id-less row at a shared live endpoint stays visible (with its Forget control) instead of being filtered out. This looks like a stale re-listing of the round-1 finding against the original commit.