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
package/dist/Discovery.d.ts
CHANGED
|
@@ -5,55 +5,68 @@ export interface DiscoveryResult {
|
|
|
5
5
|
latency: number;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* Device discovery for finding Yaver agents
|
|
8
|
+
* Device discovery for finding Yaver agents.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 2
|
|
13
|
-
*
|
|
10
|
+
* **Convex is the primary source of truth.** The user's Convex account
|
|
11
|
+
* has the freshest IP / port for each registered agent, updated every
|
|
12
|
+
* 2 minutes via heartbeat. The SDK should therefore:
|
|
13
|
+
* 1. On every `discover()` call, re-query Convex for the latest IP
|
|
14
|
+
* (no local cache shortcut when `convexUrl` + `authToken` are
|
|
15
|
+
* available).
|
|
16
|
+
* 2. Dedup the returned list (Convex can carry stale rows after
|
|
17
|
+
* re-pair) and pick the freshest online machine.
|
|
18
|
+
* 3. Probe the machine's `quicHost:httpPort` directly.
|
|
19
|
+
* 4. If the direct probe fails (different LAN / roaming), route
|
|
20
|
+
* through the configured relay.
|
|
21
|
+
* 5. Store the successful URL only AFTER the probe confirms it's
|
|
22
|
+
* reachable. Stored cache is used only as a last-chance shortcut
|
|
23
|
+
* when Convex itself is unreachable.
|
|
24
|
+
*
|
|
25
|
+
* Compared to the previous implementation this removes the "trust
|
|
26
|
+
* stored URL first" shortcut that caused the SDK to keep trying a dead
|
|
27
|
+
* cached IP long after the Mac's IP rotated.
|
|
14
28
|
*/
|
|
15
29
|
export declare class YaverDiscovery {
|
|
16
|
-
/**
|
|
17
|
-
* Discover an agent. Tries Convex cloud first (if configured),
|
|
18
|
-
* then stored connection, then LAN scan.
|
|
19
|
-
*/
|
|
20
30
|
static discover(options?: {
|
|
21
31
|
convexUrl?: string;
|
|
22
32
|
authToken?: string;
|
|
23
33
|
preferredDeviceId?: string;
|
|
24
34
|
}): Promise<DiscoveryResult | null>;
|
|
25
35
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
36
|
+
* Re-query Convex ignoring any cached URL. Intended for the call
|
|
37
|
+
* site right after a probe/network failure — it's the "the IP
|
|
38
|
+
* probably changed, ask the source of truth again" path.
|
|
28
39
|
*/
|
|
29
|
-
static
|
|
40
|
+
static refreshFromConvex(options: {
|
|
41
|
+
convexUrl: string;
|
|
42
|
+
authToken: string;
|
|
43
|
+
preferredDeviceId?: string;
|
|
44
|
+
}): Promise<DiscoveryResult | null>;
|
|
30
45
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `{relayHttpUrl}/d/{deviceId}/health` to reach the agent over the internet.
|
|
46
|
+
* Fetch the agent URL from Convex. Dedups rows, prefers fresh ones,
|
|
47
|
+
* falls back to relay if the direct LAN IP isn't reachable.
|
|
34
48
|
*/
|
|
35
|
-
static
|
|
49
|
+
static discoverFromConvex(convexUrl: string, authToken: string, preferredDeviceId?: string): Promise<DiscoveryResult | null>;
|
|
36
50
|
/**
|
|
37
|
-
*
|
|
51
|
+
* Race `/health` probes across N URLs in parallel. First 200 wins;
|
|
52
|
+
* everything else is abandoned. Mirrors the mobile app's
|
|
53
|
+
* `raceDirectCandidates` pattern — the single most reliable thing it
|
|
54
|
+
* does on same-LAN.
|
|
38
55
|
*/
|
|
39
|
-
static
|
|
56
|
+
static raceProbe(urls: string[]): Promise<DiscoveryResult | null>;
|
|
40
57
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
58
|
+
* Discover agent via relay HTTP proxy. Uses the user's configured
|
|
59
|
+
* relay first (from `/auth/validate`), then the platform relay list.
|
|
43
60
|
*/
|
|
61
|
+
static discoverViaRelay(convexUrl: string, authToken: string, deviceId: string): Promise<DiscoveryResult | null>;
|
|
62
|
+
static probeWithHeaders(url: string, headers: Record<string, string>): Promise<DiscoveryResult | null>;
|
|
63
|
+
/** Probe a specific URL for a running Yaver agent (2.5 s timeout). */
|
|
44
64
|
static probe(url: string): Promise<DiscoveryResult | null>;
|
|
45
|
-
/**
|
|
46
|
-
* Manually connect to a specific agent URL.
|
|
47
|
-
* Probes the URL and stores the connection if successful.
|
|
48
|
-
*/
|
|
49
65
|
static connect(url: string): Promise<DiscoveryResult | null>;
|
|
50
|
-
/** Get the cached agent connection from storage. */
|
|
51
66
|
static getStored(): Promise<{
|
|
52
67
|
url: string;
|
|
53
68
|
hostname: string;
|
|
54
69
|
} | null>;
|
|
55
|
-
/** Store a successful discovery result. */
|
|
56
70
|
static store(result: DiscoveryResult): Promise<void>;
|
|
57
|
-
/** Clear the stored agent connection. */
|
|
58
71
|
static clear(): Promise<void>;
|
|
59
72
|
}
|
package/dist/Discovery.js
CHANGED
|
@@ -14,27 +14,43 @@ function getAsyncStorage() {
|
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
const deviceDedup_1 = require("./deviceDedup");
|
|
17
18
|
const STORAGE_KEY = 'yaver_feedback_agent';
|
|
18
19
|
const DEFAULT_PORT = 18080;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const PROBE_TIMEOUT_MS = 2500;
|
|
21
|
+
const RELAY_PROBE_TIMEOUT_MS = 6000;
|
|
22
|
+
// LAN fallback sweep used ONLY when Convex lookup fails AND the stored
|
|
23
|
+
// cache probe fails. Keep tight — covers 192.168.1/0.x and 10.0.0/1.x
|
|
24
|
+
// with a handful of common host suffixes. The primary path is always
|
|
25
|
+
// Convex.
|
|
26
|
+
const LAN_SUBNETS = ['192.168.1', '192.168.0', '10.0.0', '10.0.1'];
|
|
27
|
+
const LAN_HOST_SUFFIXES = [1, 2, 50, 100, 101, 200];
|
|
23
28
|
/**
|
|
24
|
-
* Device discovery for finding Yaver agents
|
|
29
|
+
* Device discovery for finding Yaver agents.
|
|
25
30
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* 2
|
|
29
|
-
*
|
|
31
|
+
* **Convex is the primary source of truth.** The user's Convex account
|
|
32
|
+
* has the freshest IP / port for each registered agent, updated every
|
|
33
|
+
* 2 minutes via heartbeat. The SDK should therefore:
|
|
34
|
+
* 1. On every `discover()` call, re-query Convex for the latest IP
|
|
35
|
+
* (no local cache shortcut when `convexUrl` + `authToken` are
|
|
36
|
+
* available).
|
|
37
|
+
* 2. Dedup the returned list (Convex can carry stale rows after
|
|
38
|
+
* re-pair) and pick the freshest online machine.
|
|
39
|
+
* 3. Probe the machine's `quicHost:httpPort` directly.
|
|
40
|
+
* 4. If the direct probe fails (different LAN / roaming), route
|
|
41
|
+
* through the configured relay.
|
|
42
|
+
* 5. Store the successful URL only AFTER the probe confirms it's
|
|
43
|
+
* reachable. Stored cache is used only as a last-chance shortcut
|
|
44
|
+
* when Convex itself is unreachable.
|
|
45
|
+
*
|
|
46
|
+
* Compared to the previous implementation this removes the "trust
|
|
47
|
+
* stored URL first" shortcut that caused the SDK to keep trying a dead
|
|
48
|
+
* cached IP long after the Mac's IP rotated.
|
|
30
49
|
*/
|
|
31
50
|
class YaverDiscovery {
|
|
32
|
-
/**
|
|
33
|
-
* Discover an agent. Tries Convex cloud first (if configured),
|
|
34
|
-
* then stored connection, then LAN scan.
|
|
35
|
-
*/
|
|
36
51
|
static async discover(options) {
|
|
37
|
-
// Strategy 1: Convex
|
|
52
|
+
// Strategy 1: Convex — always tried first when credentials are
|
|
53
|
+
// available. No cache shortcut.
|
|
38
54
|
if (options?.convexUrl && options?.authToken) {
|
|
39
55
|
const result = await YaverDiscovery.discoverFromConvex(options.convexUrl, options.authToken, options.preferredDeviceId);
|
|
40
56
|
if (result) {
|
|
@@ -42,19 +58,22 @@ class YaverDiscovery {
|
|
|
42
58
|
return result;
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
|
-
// Strategy 2:
|
|
61
|
+
// Strategy 2: Stored URL. Only used as a fallback when Convex was
|
|
62
|
+
// unreachable. A successful probe here means either the mobile is
|
|
63
|
+
// offline or Convex is — we'll trust the stored IP.
|
|
46
64
|
const stored = await YaverDiscovery.getStored();
|
|
47
65
|
if (stored) {
|
|
48
66
|
const result = await YaverDiscovery.probe(stored.url);
|
|
49
|
-
if (result)
|
|
67
|
+
if (result)
|
|
50
68
|
return result;
|
|
51
|
-
}
|
|
52
69
|
await YaverDiscovery.clear();
|
|
53
70
|
}
|
|
54
|
-
// Strategy 3:
|
|
71
|
+
// Strategy 3: LAN fallback. Small sweep of common subnets. This is
|
|
72
|
+
// only hit when the user has no Convex session (device-local mode)
|
|
73
|
+
// or both Convex + cache lookups failed.
|
|
55
74
|
const candidates = [];
|
|
56
|
-
for (const subnet of
|
|
57
|
-
for (const suffix of
|
|
75
|
+
for (const subnet of LAN_SUBNETS) {
|
|
76
|
+
for (const suffix of LAN_HOST_SUFFIXES) {
|
|
58
77
|
candidates.push(`http://${subnet}.${suffix}:${DEFAULT_PORT}`);
|
|
59
78
|
}
|
|
60
79
|
}
|
|
@@ -68,13 +87,26 @@ class YaverDiscovery {
|
|
|
68
87
|
return null;
|
|
69
88
|
}
|
|
70
89
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
90
|
+
* Re-query Convex ignoring any cached URL. Intended for the call
|
|
91
|
+
* site right after a probe/network failure — it's the "the IP
|
|
92
|
+
* probably changed, ask the source of truth again" path.
|
|
93
|
+
*/
|
|
94
|
+
static async refreshFromConvex(options) {
|
|
95
|
+
await YaverDiscovery.clear();
|
|
96
|
+
const result = await YaverDiscovery.discoverFromConvex(options.convexUrl, options.authToken, options.preferredDeviceId);
|
|
97
|
+
if (result)
|
|
98
|
+
await YaverDiscovery.store(result);
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Fetch the agent URL from Convex. Dedups rows, prefers fresh ones,
|
|
103
|
+
* falls back to relay if the direct LAN IP isn't reachable.
|
|
73
104
|
*/
|
|
74
105
|
static async discoverFromConvex(convexUrl, authToken, preferredDeviceId) {
|
|
75
106
|
const base = convexUrl.replace(/\/$/, '');
|
|
76
107
|
try {
|
|
77
|
-
// Try cloud machines first (CPU/GPU managed machines)
|
|
108
|
+
// Try cloud machines first (CPU/GPU managed machines). These are
|
|
109
|
+
// long-lived with stable IPs so the direct probe is cheap.
|
|
78
110
|
const machinesRes = await fetch(`${base}/machines`, {
|
|
79
111
|
headers: { Authorization: `Bearer ${authToken}` },
|
|
80
112
|
});
|
|
@@ -88,27 +120,71 @@ class YaverDiscovery {
|
|
|
88
120
|
return probed;
|
|
89
121
|
}
|
|
90
122
|
}
|
|
91
|
-
// Fall back to
|
|
123
|
+
// Fall back to personal devices registered in Convex.
|
|
92
124
|
const devicesRes = await fetch(`${base}/devices/list`, {
|
|
93
125
|
headers: { Authorization: `Bearer ${authToken}` },
|
|
94
126
|
});
|
|
95
127
|
if (!devicesRes.ok)
|
|
96
128
|
return null;
|
|
97
129
|
const data = await devicesRes.json();
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
130
|
+
const rawList = Array.isArray(data?.devices) ? data.devices : data;
|
|
131
|
+
if (!Array.isArray(rawList) || rawList.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
// Normalise Convex fields → RemoteDevice shape so dedup works the
|
|
134
|
+
// same way `listReachableDevices` does it.
|
|
135
|
+
const normalised = rawList.map((d) => ({
|
|
136
|
+
deviceId: d.deviceId ?? d.id,
|
|
137
|
+
name: d.name ?? '',
|
|
138
|
+
platform: d.platform ?? d.os ?? '',
|
|
139
|
+
isOnline: !!d.isOnline,
|
|
140
|
+
needsAuth: !!d.needsAuth,
|
|
141
|
+
runnerDown: !!d.runnerDown,
|
|
142
|
+
lastHeartbeat: d.lastHeartbeat ?? 0,
|
|
143
|
+
isGuest: !!d.isGuest,
|
|
144
|
+
hostName: d.hostName,
|
|
145
|
+
hostEmail: d.hostEmail,
|
|
146
|
+
accessScope: d.accessScope ?? 'owner',
|
|
147
|
+
quicHost: d.quicHost ?? d.host ?? '',
|
|
148
|
+
quicPort: d.quicPort ?? 0,
|
|
149
|
+
httpPort: d.httpPort ?? d.quicPort,
|
|
150
|
+
publicKey: d.publicKey,
|
|
151
|
+
hwid: d.hardwareId ?? d.hwid,
|
|
152
|
+
localIps: Array.isArray(d.localIps)
|
|
153
|
+
? d.localIps
|
|
154
|
+
: Array.isArray(d.lanIps)
|
|
155
|
+
? d.lanIps
|
|
156
|
+
: undefined,
|
|
157
|
+
}));
|
|
158
|
+
const deduped = (0, deviceDedup_1.collapseRemoteDevices)(normalised);
|
|
159
|
+
const target = (0, deviceDedup_1.pickTargetDevice)(deduped, preferredDeviceId);
|
|
160
|
+
if (!target)
|
|
104
161
|
return null;
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
162
|
+
// Build the same candidate set the Yaver mobile app races on: the
|
|
163
|
+
// primary `quicHost` plus every LAN IP reported in the latest
|
|
164
|
+
// heartbeat (`localIps`). Multi-homed hosts commonly advertise
|
|
165
|
+
// en0 + utun (tailscale) + docker0 etc.; probing all of them in
|
|
166
|
+
// parallel makes the SDK "just work" on the same Wi-Fi without
|
|
167
|
+
// depending on which NIC the user's router DHCP'd them from.
|
|
168
|
+
const port = target.httpPort ?? target.quicPort ?? DEFAULT_PORT;
|
|
169
|
+
const ipSet = new Set();
|
|
170
|
+
if (target.quicHost)
|
|
171
|
+
ipSet.add(target.quicHost);
|
|
172
|
+
for (const ip of target.localIps ?? []) {
|
|
173
|
+
if (ip)
|
|
174
|
+
ipSet.add(ip);
|
|
175
|
+
}
|
|
176
|
+
const candidates = Array.from(ipSet).map((ip) => `http://${ip}:${port}`);
|
|
177
|
+
if (candidates.length > 0) {
|
|
178
|
+
const direct = await YaverDiscovery.raceProbe(candidates);
|
|
179
|
+
if (direct)
|
|
180
|
+
return direct;
|
|
181
|
+
}
|
|
182
|
+
// Warn if the chosen target looks stale — informative only; we
|
|
183
|
+
// still fell through to the relay path below.
|
|
184
|
+
const stale = target.lastHeartbeat &&
|
|
185
|
+
Date.now() - target.lastHeartbeat > deviceDedup_1.HEARTBEAT_STALE_MS;
|
|
186
|
+
void stale;
|
|
187
|
+
// Direct probes all failed — route through relay.
|
|
112
188
|
const relayResult = await YaverDiscovery.discoverViaRelay(base, authToken, target.deviceId);
|
|
113
189
|
if (relayResult)
|
|
114
190
|
return relayResult;
|
|
@@ -119,13 +195,51 @@ class YaverDiscovery {
|
|
|
119
195
|
}
|
|
120
196
|
}
|
|
121
197
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* `
|
|
198
|
+
* Race `/health` probes across N URLs in parallel. First 200 wins;
|
|
199
|
+
* everything else is abandoned. Mirrors the mobile app's
|
|
200
|
+
* `raceDirectCandidates` pattern — the single most reliable thing it
|
|
201
|
+
* does on same-LAN.
|
|
202
|
+
*/
|
|
203
|
+
static async raceProbe(urls) {
|
|
204
|
+
if (!urls || urls.length === 0)
|
|
205
|
+
return null;
|
|
206
|
+
const attempts = urls.map((url) => YaverDiscovery.probe(url).then((r) => {
|
|
207
|
+
if (!r)
|
|
208
|
+
throw new Error('no-200');
|
|
209
|
+
return r;
|
|
210
|
+
}));
|
|
211
|
+
try {
|
|
212
|
+
// `Promise.any` isn't on every RN runtime yet (older Hermes).
|
|
213
|
+
// Hand-roll the same behaviour so we don't need a polyfill.
|
|
214
|
+
return await new Promise((resolve) => {
|
|
215
|
+
let remaining = attempts.length;
|
|
216
|
+
let settled = false;
|
|
217
|
+
attempts.forEach((p) => {
|
|
218
|
+
p.then((r) => {
|
|
219
|
+
if (settled)
|
|
220
|
+
return;
|
|
221
|
+
settled = true;
|
|
222
|
+
resolve(r);
|
|
223
|
+
}).catch(() => {
|
|
224
|
+
remaining -= 1;
|
|
225
|
+
if (remaining <= 0 && !settled) {
|
|
226
|
+
settled = true;
|
|
227
|
+
resolve(null);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Discover agent via relay HTTP proxy. Uses the user's configured
|
|
239
|
+
* relay first (from `/auth/validate`), then the platform relay list.
|
|
125
240
|
*/
|
|
126
241
|
static async discoverViaRelay(convexUrl, authToken, deviceId) {
|
|
127
242
|
try {
|
|
128
|
-
// Fetch relay server list from user settings first, then platform config
|
|
129
243
|
const settingsRes = await fetch(`${convexUrl}/auth/validate`, {
|
|
130
244
|
headers: { Authorization: `Bearer ${authToken}` },
|
|
131
245
|
});
|
|
@@ -136,7 +250,6 @@ class YaverDiscovery {
|
|
|
136
250
|
relayUrl = settingsData.relayUrl;
|
|
137
251
|
relayPassword = settingsData.relayPassword;
|
|
138
252
|
}
|
|
139
|
-
// If no user-level relay, fetch platform relay servers
|
|
140
253
|
if (!relayUrl) {
|
|
141
254
|
const configRes = await fetch(`${convexUrl}/platform-config?key=relay_servers`);
|
|
142
255
|
if (configRes.ok) {
|
|
@@ -145,36 +258,29 @@ class YaverDiscovery {
|
|
|
145
258
|
? JSON.parse(configData.value)
|
|
146
259
|
: configData.value;
|
|
147
260
|
if (Array.isArray(servers) && servers.length > 0) {
|
|
148
|
-
// Pick the first (highest priority) relay with an httpUrl
|
|
149
261
|
const relay = servers.find((s) => s.httpUrl);
|
|
150
|
-
if (relay)
|
|
262
|
+
if (relay)
|
|
151
263
|
relayUrl = relay.httpUrl;
|
|
152
|
-
}
|
|
153
264
|
}
|
|
154
265
|
}
|
|
155
266
|
}
|
|
156
267
|
if (!relayUrl)
|
|
157
268
|
return null;
|
|
158
|
-
// Probe agent through relay: {relayHttpUrl}/d/{deviceId}/health
|
|
159
269
|
const relayBase = `${relayUrl.replace(/\/$/, '')}/d/${deviceId}`;
|
|
160
|
-
|
|
270
|
+
return YaverDiscovery.probeWithHeaders(relayBase, {
|
|
161
271
|
'X-Relay-Password': relayPassword || '',
|
|
162
272
|
});
|
|
163
|
-
return result;
|
|
164
273
|
}
|
|
165
274
|
catch {
|
|
166
275
|
return null;
|
|
167
276
|
}
|
|
168
277
|
}
|
|
169
|
-
/**
|
|
170
|
-
* Probe with extra headers (e.g. relay password).
|
|
171
|
-
*/
|
|
172
278
|
static async probeWithHeaders(url, headers) {
|
|
173
279
|
const base = url.replace(/\/$/, '');
|
|
174
280
|
const start = Date.now();
|
|
175
281
|
try {
|
|
176
282
|
const controller = new AbortController();
|
|
177
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
283
|
+
const timeoutId = setTimeout(() => controller.abort(), RELAY_PROBE_TIMEOUT_MS);
|
|
178
284
|
const response = await fetch(`${base}/health`, {
|
|
179
285
|
method: 'GET',
|
|
180
286
|
headers,
|
|
@@ -192,7 +298,7 @@ class YaverDiscovery {
|
|
|
192
298
|
version = data.version ?? 'unknown';
|
|
193
299
|
}
|
|
194
300
|
catch {
|
|
195
|
-
//
|
|
301
|
+
// /health may return plain text
|
|
196
302
|
}
|
|
197
303
|
return { url: base, hostname, version, latency };
|
|
198
304
|
}
|
|
@@ -200,24 +306,20 @@ class YaverDiscovery {
|
|
|
200
306
|
return null;
|
|
201
307
|
}
|
|
202
308
|
}
|
|
203
|
-
/**
|
|
204
|
-
* Probe a specific URL for a running Yaver agent.
|
|
205
|
-
* Hits the `/health` endpoint with a 2s timeout.
|
|
206
|
-
*/
|
|
309
|
+
/** Probe a specific URL for a running Yaver agent (2.5 s timeout). */
|
|
207
310
|
static async probe(url) {
|
|
208
311
|
const base = url.replace(/\/$/, '');
|
|
209
312
|
const start = Date.now();
|
|
210
313
|
try {
|
|
211
314
|
const controller = new AbortController();
|
|
212
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
315
|
+
const timeoutId = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
213
316
|
const response = await fetch(`${base}/health`, {
|
|
214
317
|
method: 'GET',
|
|
215
318
|
signal: controller.signal,
|
|
216
319
|
});
|
|
217
320
|
clearTimeout(timeoutId);
|
|
218
|
-
if (!response.ok)
|
|
321
|
+
if (!response.ok)
|
|
219
322
|
return null;
|
|
220
|
-
}
|
|
221
323
|
const latency = Date.now() - start;
|
|
222
324
|
let hostname = 'Unknown';
|
|
223
325
|
let version = 'unknown';
|
|
@@ -227,7 +329,7 @@ class YaverDiscovery {
|
|
|
227
329
|
version = data.version ?? 'unknown';
|
|
228
330
|
}
|
|
229
331
|
catch {
|
|
230
|
-
//
|
|
332
|
+
// /health may return plain text
|
|
231
333
|
}
|
|
232
334
|
return { url: base, hostname, version, latency };
|
|
233
335
|
}
|
|
@@ -235,18 +337,12 @@ class YaverDiscovery {
|
|
|
235
337
|
return null;
|
|
236
338
|
}
|
|
237
339
|
}
|
|
238
|
-
/**
|
|
239
|
-
* Manually connect to a specific agent URL.
|
|
240
|
-
* Probes the URL and stores the connection if successful.
|
|
241
|
-
*/
|
|
242
340
|
static async connect(url) {
|
|
243
341
|
const result = await YaverDiscovery.probe(url);
|
|
244
|
-
if (result)
|
|
342
|
+
if (result)
|
|
245
343
|
await YaverDiscovery.store(result);
|
|
246
|
-
}
|
|
247
344
|
return result;
|
|
248
345
|
}
|
|
249
|
-
/** Get the cached agent connection from storage. */
|
|
250
346
|
static async getStored() {
|
|
251
347
|
const storage = getAsyncStorage();
|
|
252
348
|
if (!storage)
|
|
@@ -265,7 +361,6 @@ class YaverDiscovery {
|
|
|
265
361
|
return null;
|
|
266
362
|
}
|
|
267
363
|
}
|
|
268
|
-
/** Store a successful discovery result. */
|
|
269
364
|
static async store(result) {
|
|
270
365
|
const storage = getAsyncStorage();
|
|
271
366
|
if (!storage)
|
|
@@ -277,7 +372,6 @@ class YaverDiscovery {
|
|
|
277
372
|
// Storage failure is non-fatal
|
|
278
373
|
}
|
|
279
374
|
}
|
|
280
|
-
/** Clear the stored agent connection. */
|
|
281
375
|
static async clear() {
|
|
282
376
|
const storage = getAsyncStorage();
|
|
283
377
|
if (!storage)
|
package/dist/FeedbackModal.js
CHANGED
|
@@ -75,17 +75,44 @@ const FeedbackModal = () => {
|
|
|
75
75
|
setToast(null);
|
|
76
76
|
setAction('idle');
|
|
77
77
|
}, []);
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
// Helper: run a P2P call; on network failure, ask YaverFeedback to
|
|
79
|
+
// re-query Convex for the fresh IP and retry once. Solves the common
|
|
80
|
+
// case where the Mac's LAN IP rotated while the SDK held a stale URL.
|
|
81
|
+
const runWithReconnect = (0, react_1.useCallback)(async (fn) => {
|
|
82
|
+
let client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
81
83
|
if (!client) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
const ok = await YaverFeedback_1.YaverFeedback.reconnect();
|
|
85
|
+
if (ok)
|
|
86
|
+
client = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
87
|
+
}
|
|
88
|
+
if (!client) {
|
|
89
|
+
throw new Error('Not connected to the agent yet.');
|
|
84
90
|
}
|
|
91
|
+
try {
|
|
92
|
+
await fn(client);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
const transient = /Network request failed|timeout|ECONNREFUSED|Failed to fetch|fetch failed|aborted/i.test(msg);
|
|
97
|
+
if (!transient)
|
|
98
|
+
throw err;
|
|
99
|
+
const ok = await YaverFeedback_1.YaverFeedback.reconnect();
|
|
100
|
+
if (!ok)
|
|
101
|
+
throw err;
|
|
102
|
+
const fresh = YaverFeedback_1.YaverFeedback.getP2PClient();
|
|
103
|
+
if (!fresh)
|
|
104
|
+
throw err;
|
|
105
|
+
await fn(fresh);
|
|
106
|
+
}
|
|
107
|
+
}, []);
|
|
108
|
+
// ─── 1. Hot reload ─────────────────────────────────────────────────
|
|
109
|
+
const handleHotReload = (0, react_1.useCallback)(async () => {
|
|
85
110
|
setAction('hot-reloading');
|
|
86
111
|
setError(null);
|
|
87
112
|
try {
|
|
88
|
-
await client
|
|
113
|
+
await runWithReconnect(async (client) => {
|
|
114
|
+
await client.reloadApp('dev');
|
|
115
|
+
});
|
|
89
116
|
setToast('Reload sent');
|
|
90
117
|
closeSoon(800);
|
|
91
118
|
}
|
|
@@ -96,7 +123,7 @@ const FeedbackModal = () => {
|
|
|
96
123
|
if (mountedRef.current)
|
|
97
124
|
setAction('idle');
|
|
98
125
|
}
|
|
99
|
-
}, [closeSoon]);
|
|
126
|
+
}, [closeSoon, runWithReconnect]);
|
|
100
127
|
// ─── 2. Screenshot + Fix ───────────────────────────────────────────
|
|
101
128
|
//
|
|
102
129
|
// Hide the modal first so the screenshot captures the actual screen
|
|
@@ -79,7 +79,11 @@ const YaverMachinePickerScreen = ({ token, currentDeviceId, onPick, onCancel, })
|
|
|
79
79
|
};
|
|
80
80
|
const renderDevice = (device) => {
|
|
81
81
|
const selected = device.deviceId === currentDeviceId;
|
|
82
|
-
|
|
82
|
+
// Match the Yaver mobile app: HEARTBEAT_STALE_MS is 90 s. Using
|
|
83
|
+
// 60 s here flashed yellow on a single missed agent beat even
|
|
84
|
+
// though the Mac was up.
|
|
85
|
+
const stale = device.lastHeartbeat > 0 &&
|
|
86
|
+
Date.now() - device.lastHeartbeat > 90000;
|
|
83
87
|
const healthColor = !device.isOnline
|
|
84
88
|
? '#ef4444'
|
|
85
89
|
: device.needsAuth || device.runnerDown || stale
|
package/dist/YaverFeedback.d.ts
CHANGED
|
@@ -19,6 +19,16 @@ export declare class YaverFeedback {
|
|
|
19
19
|
* Sets config.agentUrl and creates P2PClient on success.
|
|
20
20
|
*/
|
|
21
21
|
static discoverAgent(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Force a fresh Convex lookup for the agent URL — ignoring any
|
|
24
|
+
* cached URL. Callers use this after a P2P request fails
|
|
25
|
+
* (connection refused / timeout) because the most common cause is
|
|
26
|
+
* the Mac's LAN IP rotating. Convex has the fresh one, so we
|
|
27
|
+
* re-query and probe `[quicHost, ...localIps]` in parallel.
|
|
28
|
+
*
|
|
29
|
+
* Returns true when a new URL was adopted.
|
|
30
|
+
*/
|
|
31
|
+
static reconnect(): Promise<boolean>;
|
|
22
32
|
/**
|
|
23
33
|
* Pull a cached session token + selected device from AsyncStorage (populated
|
|
24
34
|
* by the in-SDK login + machine-picker screens). When present the SDK can
|
package/dist/YaverFeedback.js
CHANGED
|
@@ -188,6 +188,36 @@ class YaverFeedback {
|
|
|
188
188
|
// Discovery failed — FloatingButton will show disconnected, user can retry
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Force a fresh Convex lookup for the agent URL — ignoring any
|
|
193
|
+
* cached URL. Callers use this after a P2P request fails
|
|
194
|
+
* (connection refused / timeout) because the most common cause is
|
|
195
|
+
* the Mac's LAN IP rotating. Convex has the fresh one, so we
|
|
196
|
+
* re-query and probe `[quicHost, ...localIps]` in parallel.
|
|
197
|
+
*
|
|
198
|
+
* Returns true when a new URL was adopted.
|
|
199
|
+
*/
|
|
200
|
+
static async reconnect() {
|
|
201
|
+
if (!config || !enabled)
|
|
202
|
+
return false;
|
|
203
|
+
if (!config.authToken || !config.convexUrl)
|
|
204
|
+
return false;
|
|
205
|
+
try {
|
|
206
|
+
const result = await Discovery_1.YaverDiscovery.refreshFromConvex({
|
|
207
|
+
convexUrl: config.convexUrl,
|
|
208
|
+
authToken: config.authToken,
|
|
209
|
+
preferredDeviceId: config.preferredDeviceId,
|
|
210
|
+
});
|
|
211
|
+
if (!result)
|
|
212
|
+
return false;
|
|
213
|
+
config.agentUrl = result.url;
|
|
214
|
+
p2pClient = new P2PClient_1.P2PClient(result.url, config.authToken ?? '');
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
191
221
|
/**
|
|
192
222
|
* Pull a cached session token + selected device from AsyncStorage (populated
|
|
193
223
|
* by the in-SDK login + machine-picker screens). When present the SDK can
|
package/dist/auth.d.ts
CHANGED
|
@@ -105,7 +105,17 @@ export interface RemoteDevice {
|
|
|
105
105
|
accessScope: 'owner' | 'shared-scoped' | 'shared-legacy';
|
|
106
106
|
quicHost: string;
|
|
107
107
|
quicPort: number;
|
|
108
|
+
/** Agent HTTP port — preferred over quicPort when present. */
|
|
109
|
+
httpPort?: number;
|
|
108
110
|
publicKey?: string;
|
|
111
|
+
/** Hardware identifier — used for dedup across re-pair events. */
|
|
112
|
+
hwid?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Every LAN IP the agent reported in its last heartbeat. Useful on
|
|
115
|
+
* multi-homed hosts — probing all of them in parallel is the same
|
|
116
|
+
* trick the Yaver mobile app uses to "just work" on the same Wi-Fi.
|
|
117
|
+
*/
|
|
118
|
+
localIps?: string[];
|
|
109
119
|
}
|
|
110
120
|
export interface DeviceList {
|
|
111
121
|
owned: RemoteDevice[];
|
|
@@ -114,5 +124,9 @@ export interface DeviceList {
|
|
|
114
124
|
/**
|
|
115
125
|
* Fetch the set of remote dev machines this user can reach. Splits into
|
|
116
126
|
* owned (user is the host) vs shared (host invited them as a guest).
|
|
127
|
+
*
|
|
128
|
+
* Collapses duplicate rows before splitting — Convex can return multiple
|
|
129
|
+
* rows per physical machine after a re-pair or hostname change, and the
|
|
130
|
+
* raw list used to render as "Kvancs-MacBook-Air.local ×3" in the picker.
|
|
117
131
|
*/
|
|
118
132
|
export declare function listReachableDevices(token: string): Promise<DeviceList>;
|