yaver-feedback-react-native 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Discovery.d.ts +40 -27
- package/dist/Discovery.js +164 -70
- package/dist/FeedbackModal.js +34 -7
- package/dist/MachinePickerScreen.js +5 -1
- package/dist/YaverFeedback.d.ts +10 -0
- package/dist/YaverFeedback.js +30 -0
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +36 -3
- package/dist/deviceDedup.d.ts +44 -0
- package/dist/deviceDedup.js +184 -0
- package/package.json +1 -1
- package/src/Discovery.ts +188 -101
- package/src/FeedbackModal.tsx +33 -7
- package/src/MachinePickerScreen.tsx +6 -1
- package/src/YaverFeedback.ts +27 -0
- package/src/auth.ts +46 -3
- package/src/deviceDedup.ts +184 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device deduplication + online-signal merging, ported from the Yaver
|
|
3
|
+
* mobile app's DeviceContext.collapseAliasDevices.
|
|
4
|
+
*
|
|
5
|
+
* Convex stores one row per pair (device, re-install). After a re-pair
|
|
6
|
+
* or hostname change the list can contain 2-3 rows for the same
|
|
7
|
+
* physical machine, with different hwid/publicKey values. The picker
|
|
8
|
+
* then shows duplicates and the user can't tell which one is live.
|
|
9
|
+
*
|
|
10
|
+
* This file collapses rows in three passes:
|
|
11
|
+
* 1. Identity key (hwid → publicKey → "host:os:name" → id → name)
|
|
12
|
+
* 2. Alias key (os + normalized-hostname) — catches re-pairs that
|
|
13
|
+
* mint a new hwid
|
|
14
|
+
* 3. Endpoint key (host:port)
|
|
15
|
+
*
|
|
16
|
+
* When collapsing, `mergeDeviceEntries` prefers: authenticated over
|
|
17
|
+
* needsAuth, online over offline, freshest lastHeartbeat.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { RemoteDevice } from './auth';
|
|
21
|
+
|
|
22
|
+
function normalizedName(name: string | undefined): string {
|
|
23
|
+
return String(name || '').trim().toLowerCase().replace(/\.local$/i, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizedHost(host: string | undefined): string {
|
|
27
|
+
return String(host || '').trim().toLowerCase().replace(/\.local$/i, '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function identityKey(d: RemoteDevice): string {
|
|
31
|
+
if (d.hwid) return `hwid:${d.hwid}`;
|
|
32
|
+
if (d.publicKey) return `pub:${d.publicKey}`;
|
|
33
|
+
if (d.isGuest) {
|
|
34
|
+
const scope = d.hostEmail || d.hostName || 'guest';
|
|
35
|
+
return `guest:${scope}:${d.deviceId || d.name}`;
|
|
36
|
+
}
|
|
37
|
+
const n = normalizedName(d.name);
|
|
38
|
+
const os = String(d.platform || '').trim().toLowerCase();
|
|
39
|
+
if (n && os) return `host:${os}:${n}`;
|
|
40
|
+
if (d.deviceId) return `id:${d.deviceId}`;
|
|
41
|
+
return `name:${d.name}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function aliasKey(d: RemoteDevice): string | null {
|
|
45
|
+
if (d.isGuest) return null;
|
|
46
|
+
const n = normalizedName(d.name);
|
|
47
|
+
const os = String(d.platform || '').trim().toLowerCase();
|
|
48
|
+
if (!n || !os) return null;
|
|
49
|
+
return `${os}:${n}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function endpointKey(d: RemoteDevice): string | null {
|
|
53
|
+
if (d.isGuest) return null;
|
|
54
|
+
const h = normalizedHost(d.quicHost);
|
|
55
|
+
if (!h) return null;
|
|
56
|
+
return `${h}:${d.quicPort || 0}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mergeEntries(existing: RemoteDevice, incoming: RemoteDevice): RemoteDevice {
|
|
60
|
+
const incomingWins =
|
|
61
|
+
(!!existing.needsAuth && !incoming.needsAuth) ||
|
|
62
|
+
(incoming.lastHeartbeat || 0) > (existing.lastHeartbeat || 0) ||
|
|
63
|
+
(!!incoming.isOnline && !existing.isOnline);
|
|
64
|
+
const base = incomingWins ? incoming : existing;
|
|
65
|
+
const other = incomingWins ? existing : incoming;
|
|
66
|
+
return {
|
|
67
|
+
...other,
|
|
68
|
+
...base,
|
|
69
|
+
quicHost: base.quicHost || other.quicHost,
|
|
70
|
+
quicPort: base.quicPort || other.quicPort,
|
|
71
|
+
isOnline: base.isOnline || other.isOnline,
|
|
72
|
+
runnerDown: base.runnerDown && other.runnerDown,
|
|
73
|
+
publicKey: base.publicKey || other.publicKey,
|
|
74
|
+
lastHeartbeat: Math.max(existing.lastHeartbeat || 0, incoming.lastHeartbeat || 0),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// When two rows share the same alias key (hostname + OS) but differ on
|
|
79
|
+
// hwid/publicKey, pick the active one over the stale needs-auth leftover.
|
|
80
|
+
function pickActiveOverStaleNeedsAuth(a: RemoteDevice, b: RemoteDevice): RemoteDevice | null {
|
|
81
|
+
const aDead = a.needsAuth && !a.isOnline;
|
|
82
|
+
const bDead = b.needsAuth && !b.isOnline;
|
|
83
|
+
const aLive = !a.needsAuth && a.isOnline;
|
|
84
|
+
const bLive = !b.needsAuth && b.isOnline;
|
|
85
|
+
if (aDead && bLive) return b;
|
|
86
|
+
if (bDead && aLive) return a;
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Collapse a Convex device list so each physical machine appears once.
|
|
92
|
+
* Safe on an empty list; idempotent on an already-deduped list.
|
|
93
|
+
*/
|
|
94
|
+
export function collapseRemoteDevices(devices: RemoteDevice[]): RemoteDevice[] {
|
|
95
|
+
if (!Array.isArray(devices) || devices.length === 0) return [];
|
|
96
|
+
|
|
97
|
+
const byIdentity = new Map<string, RemoteDevice>();
|
|
98
|
+
for (const d of devices) {
|
|
99
|
+
const k = identityKey(d);
|
|
100
|
+
const prev = byIdentity.get(k);
|
|
101
|
+
byIdentity.set(k, prev ? mergeEntries(prev, d) : d);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const byAlias = new Map<string, RemoteDevice>();
|
|
105
|
+
for (const d of byIdentity.values()) {
|
|
106
|
+
const k = aliasKey(d);
|
|
107
|
+
if (!k) {
|
|
108
|
+
byAlias.set(`id:${d.deviceId}`, d);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const prev = byAlias.get(k);
|
|
112
|
+
if (!prev) {
|
|
113
|
+
byAlias.set(k, d);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const strongIdentityConflict =
|
|
117
|
+
!!prev.publicKey && !!d.publicKey && prev.publicKey !== d.publicKey;
|
|
118
|
+
if (strongIdentityConflict) {
|
|
119
|
+
const winner = pickActiveOverStaleNeedsAuth(prev, d);
|
|
120
|
+
if (winner) {
|
|
121
|
+
byAlias.set(k, winner);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
byAlias.set(k, mergeEntries(prev, d));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const byEndpoint = new Map<string, RemoteDevice>();
|
|
129
|
+
for (const d of byAlias.values()) {
|
|
130
|
+
const k = endpointKey(d);
|
|
131
|
+
if (!k) {
|
|
132
|
+
byEndpoint.set(`id:${d.deviceId}`, d);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const prev = byEndpoint.get(k);
|
|
136
|
+
byEndpoint.set(k, prev ? mergeEntries(prev, d) : d);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return [...byEndpoint.values()];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Threshold for "online" based on heartbeat age, in milliseconds.
|
|
144
|
+
* Matches the mobile app (HEARTBEAT_STALE_MS = 90 s). The SDK used
|
|
145
|
+
* 60 s, which flashed yellow on single missed beats.
|
|
146
|
+
*/
|
|
147
|
+
export const HEARTBEAT_STALE_MS = 90_000;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Returns a freshness flag consistent with the mobile app. A device is
|
|
151
|
+
* "fresh" when it was online per Convex AND its heartbeat is within
|
|
152
|
+
* `HEARTBEAT_STALE_MS`.
|
|
153
|
+
*/
|
|
154
|
+
export function isDeviceFresh(d: RemoteDevice): boolean {
|
|
155
|
+
if (!d.isOnline) return false;
|
|
156
|
+
if (!d.lastHeartbeat) return true;
|
|
157
|
+
return Date.now() - d.lastHeartbeat < HEARTBEAT_STALE_MS;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Pick the best candidate for an auto-connect attempt. Preference:
|
|
162
|
+
* 1. matches the preferred deviceId when supplied + still fresh
|
|
163
|
+
* 2. fresh (online + recent heartbeat) + has a quicHost
|
|
164
|
+
* 3. online + has a quicHost
|
|
165
|
+
* 4. first with a quicHost
|
|
166
|
+
*/
|
|
167
|
+
export function pickTargetDevice(
|
|
168
|
+
devices: RemoteDevice[],
|
|
169
|
+
preferredDeviceId?: string,
|
|
170
|
+
): RemoteDevice | null {
|
|
171
|
+
if (!devices.length) return null;
|
|
172
|
+
if (preferredDeviceId) {
|
|
173
|
+
const preferred = devices.find(
|
|
174
|
+
(d) => d.deviceId === preferredDeviceId && d.quicHost,
|
|
175
|
+
);
|
|
176
|
+
if (preferred && isDeviceFresh(preferred)) return preferred;
|
|
177
|
+
if (preferred) return preferred;
|
|
178
|
+
}
|
|
179
|
+
const fresh = devices.find((d) => isDeviceFresh(d) && d.quicHost);
|
|
180
|
+
if (fresh) return fresh;
|
|
181
|
+
const online = devices.find((d) => d.isOnline && d.quicHost);
|
|
182
|
+
if (online) return online;
|
|
183
|
+
return devices.find((d) => d.quicHost) || devices[0] || null;
|
|
184
|
+
}
|