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/auth.js CHANGED
@@ -346,6 +346,10 @@ async function loginWithEmail(email, password) {
346
346
  /**
347
347
  * Fetch the set of remote dev machines this user can reach. Splits into
348
348
  * owned (user is the host) vs shared (host invited them as a guest).
349
+ *
350
+ * Collapses duplicate rows before splitting — Convex can return multiple
351
+ * rows per physical machine after a re-pair or hostname change, and the
352
+ * raw list used to render as "Kvancs-MacBook-Air.local ×3" in the picker.
349
353
  */
350
354
  async function listReachableDevices(token) {
351
355
  try {
@@ -355,10 +359,39 @@ async function listReachableDevices(token) {
355
359
  if (!res.ok)
356
360
  return { owned: [], shared: [] };
357
361
  const data = await res.json();
358
- const all = (data.devices ?? []);
362
+ const raw = (data.devices ?? []);
363
+ // Normalise Convex field names → SDK's RemoteDevice shape. The
364
+ // backend returns `localIps`, sometimes the mobile-side mapping
365
+ // surfaces `lanIps` — accept either so the field survives.
366
+ const normalised = raw.map((d) => ({
367
+ deviceId: d.deviceId ?? d.id,
368
+ name: d.name ?? '',
369
+ platform: d.platform ?? d.os ?? '',
370
+ isOnline: !!d.isOnline,
371
+ needsAuth: !!d.needsAuth,
372
+ runnerDown: !!d.runnerDown,
373
+ lastHeartbeat: d.lastHeartbeat ?? 0,
374
+ isGuest: !!d.isGuest,
375
+ hostName: d.hostName,
376
+ hostEmail: d.hostEmail,
377
+ accessScope: d.accessScope ?? 'owner',
378
+ quicHost: d.quicHost ?? d.host ?? '',
379
+ quicPort: d.quicPort ?? 0,
380
+ httpPort: d.httpPort ?? d.quicPort,
381
+ publicKey: d.publicKey,
382
+ hwid: d.hardwareId ?? d.hwid,
383
+ localIps: Array.isArray(d.localIps)
384
+ ? d.localIps
385
+ : Array.isArray(d.lanIps)
386
+ ? d.lanIps
387
+ : undefined,
388
+ }));
389
+ // Lazy require so Jest + tree-shakers don't choke on a circular import.
390
+ const { collapseRemoteDevices } = require('./deviceDedup');
391
+ const deduped = collapseRemoteDevices(normalised);
359
392
  return {
360
- owned: all.filter((d) => !d.isGuest),
361
- shared: all.filter((d) => d.isGuest),
393
+ owned: deduped.filter((d) => !d.isGuest),
394
+ shared: deduped.filter((d) => d.isGuest),
362
395
  };
363
396
  }
364
397
  catch {
@@ -0,0 +1,44 @@
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
+ import type { RemoteDevice } from './auth';
20
+ /**
21
+ * Collapse a Convex device list so each physical machine appears once.
22
+ * Safe on an empty list; idempotent on an already-deduped list.
23
+ */
24
+ export declare function collapseRemoteDevices(devices: RemoteDevice[]): RemoteDevice[];
25
+ /**
26
+ * Threshold for "online" based on heartbeat age, in milliseconds.
27
+ * Matches the mobile app (HEARTBEAT_STALE_MS = 90 s). The SDK used
28
+ * 60 s, which flashed yellow on single missed beats.
29
+ */
30
+ export declare const HEARTBEAT_STALE_MS = 90000;
31
+ /**
32
+ * Returns a freshness flag consistent with the mobile app. A device is
33
+ * "fresh" when it was online per Convex AND its heartbeat is within
34
+ * `HEARTBEAT_STALE_MS`.
35
+ */
36
+ export declare function isDeviceFresh(d: RemoteDevice): boolean;
37
+ /**
38
+ * Pick the best candidate for an auto-connect attempt. Preference:
39
+ * 1. matches the preferred deviceId when supplied + still fresh
40
+ * 2. fresh (online + recent heartbeat) + has a quicHost
41
+ * 3. online + has a quicHost
42
+ * 4. first with a quicHost
43
+ */
44
+ export declare function pickTargetDevice(devices: RemoteDevice[], preferredDeviceId?: string): RemoteDevice | null;
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ /**
3
+ * Device deduplication + online-signal merging, ported from the Yaver
4
+ * mobile app's DeviceContext.collapseAliasDevices.
5
+ *
6
+ * Convex stores one row per pair (device, re-install). After a re-pair
7
+ * or hostname change the list can contain 2-3 rows for the same
8
+ * physical machine, with different hwid/publicKey values. The picker
9
+ * then shows duplicates and the user can't tell which one is live.
10
+ *
11
+ * This file collapses rows in three passes:
12
+ * 1. Identity key (hwid → publicKey → "host:os:name" → id → name)
13
+ * 2. Alias key (os + normalized-hostname) — catches re-pairs that
14
+ * mint a new hwid
15
+ * 3. Endpoint key (host:port)
16
+ *
17
+ * When collapsing, `mergeDeviceEntries` prefers: authenticated over
18
+ * needsAuth, online over offline, freshest lastHeartbeat.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.HEARTBEAT_STALE_MS = void 0;
22
+ exports.collapseRemoteDevices = collapseRemoteDevices;
23
+ exports.isDeviceFresh = isDeviceFresh;
24
+ exports.pickTargetDevice = pickTargetDevice;
25
+ function normalizedName(name) {
26
+ return String(name || '').trim().toLowerCase().replace(/\.local$/i, '');
27
+ }
28
+ function normalizedHost(host) {
29
+ return String(host || '').trim().toLowerCase().replace(/\.local$/i, '');
30
+ }
31
+ function identityKey(d) {
32
+ if (d.hwid)
33
+ return `hwid:${d.hwid}`;
34
+ if (d.publicKey)
35
+ return `pub:${d.publicKey}`;
36
+ if (d.isGuest) {
37
+ const scope = d.hostEmail || d.hostName || 'guest';
38
+ return `guest:${scope}:${d.deviceId || d.name}`;
39
+ }
40
+ const n = normalizedName(d.name);
41
+ const os = String(d.platform || '').trim().toLowerCase();
42
+ if (n && os)
43
+ return `host:${os}:${n}`;
44
+ if (d.deviceId)
45
+ return `id:${d.deviceId}`;
46
+ return `name:${d.name}`;
47
+ }
48
+ function aliasKey(d) {
49
+ if (d.isGuest)
50
+ return null;
51
+ const n = normalizedName(d.name);
52
+ const os = String(d.platform || '').trim().toLowerCase();
53
+ if (!n || !os)
54
+ return null;
55
+ return `${os}:${n}`;
56
+ }
57
+ function endpointKey(d) {
58
+ if (d.isGuest)
59
+ return null;
60
+ const h = normalizedHost(d.quicHost);
61
+ if (!h)
62
+ return null;
63
+ return `${h}:${d.quicPort || 0}`;
64
+ }
65
+ function mergeEntries(existing, incoming) {
66
+ const incomingWins = (!!existing.needsAuth && !incoming.needsAuth) ||
67
+ (incoming.lastHeartbeat || 0) > (existing.lastHeartbeat || 0) ||
68
+ (!!incoming.isOnline && !existing.isOnline);
69
+ const base = incomingWins ? incoming : existing;
70
+ const other = incomingWins ? existing : incoming;
71
+ return {
72
+ ...other,
73
+ ...base,
74
+ quicHost: base.quicHost || other.quicHost,
75
+ quicPort: base.quicPort || other.quicPort,
76
+ isOnline: base.isOnline || other.isOnline,
77
+ runnerDown: base.runnerDown && other.runnerDown,
78
+ publicKey: base.publicKey || other.publicKey,
79
+ lastHeartbeat: Math.max(existing.lastHeartbeat || 0, incoming.lastHeartbeat || 0),
80
+ };
81
+ }
82
+ // When two rows share the same alias key (hostname + OS) but differ on
83
+ // hwid/publicKey, pick the active one over the stale needs-auth leftover.
84
+ function pickActiveOverStaleNeedsAuth(a, b) {
85
+ const aDead = a.needsAuth && !a.isOnline;
86
+ const bDead = b.needsAuth && !b.isOnline;
87
+ const aLive = !a.needsAuth && a.isOnline;
88
+ const bLive = !b.needsAuth && b.isOnline;
89
+ if (aDead && bLive)
90
+ return b;
91
+ if (bDead && aLive)
92
+ return a;
93
+ return null;
94
+ }
95
+ /**
96
+ * Collapse a Convex device list so each physical machine appears once.
97
+ * Safe on an empty list; idempotent on an already-deduped list.
98
+ */
99
+ function collapseRemoteDevices(devices) {
100
+ if (!Array.isArray(devices) || devices.length === 0)
101
+ return [];
102
+ const byIdentity = new Map();
103
+ for (const d of devices) {
104
+ const k = identityKey(d);
105
+ const prev = byIdentity.get(k);
106
+ byIdentity.set(k, prev ? mergeEntries(prev, d) : d);
107
+ }
108
+ const byAlias = new Map();
109
+ for (const d of byIdentity.values()) {
110
+ const k = aliasKey(d);
111
+ if (!k) {
112
+ byAlias.set(`id:${d.deviceId}`, d);
113
+ continue;
114
+ }
115
+ const prev = byAlias.get(k);
116
+ if (!prev) {
117
+ byAlias.set(k, d);
118
+ continue;
119
+ }
120
+ const strongIdentityConflict = !!prev.publicKey && !!d.publicKey && prev.publicKey !== d.publicKey;
121
+ if (strongIdentityConflict) {
122
+ const winner = pickActiveOverStaleNeedsAuth(prev, d);
123
+ if (winner) {
124
+ byAlias.set(k, winner);
125
+ continue;
126
+ }
127
+ }
128
+ byAlias.set(k, mergeEntries(prev, d));
129
+ }
130
+ const byEndpoint = new Map();
131
+ for (const d of byAlias.values()) {
132
+ const k = endpointKey(d);
133
+ if (!k) {
134
+ byEndpoint.set(`id:${d.deviceId}`, d);
135
+ continue;
136
+ }
137
+ const prev = byEndpoint.get(k);
138
+ byEndpoint.set(k, prev ? mergeEntries(prev, d) : d);
139
+ }
140
+ return [...byEndpoint.values()];
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
+ exports.HEARTBEAT_STALE_MS = 90000;
148
+ /**
149
+ * Returns a freshness flag consistent with the mobile app. A device is
150
+ * "fresh" when it was online per Convex AND its heartbeat is within
151
+ * `HEARTBEAT_STALE_MS`.
152
+ */
153
+ function isDeviceFresh(d) {
154
+ if (!d.isOnline)
155
+ return false;
156
+ if (!d.lastHeartbeat)
157
+ return true;
158
+ return Date.now() - d.lastHeartbeat < exports.HEARTBEAT_STALE_MS;
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
+ function pickTargetDevice(devices, preferredDeviceId) {
168
+ if (!devices.length)
169
+ return null;
170
+ if (preferredDeviceId) {
171
+ const preferred = devices.find((d) => d.deviceId === preferredDeviceId && d.quicHost);
172
+ if (preferred && isDeviceFresh(preferred))
173
+ return preferred;
174
+ if (preferred)
175
+ return preferred;
176
+ }
177
+ const fresh = devices.find((d) => isDeviceFresh(d) && d.quicHost);
178
+ if (fresh)
179
+ return fresh;
180
+ const online = devices.find((d) => d.isOnline && d.quicHost);
181
+ if (online)
182
+ return online;
183
+ return devices.find((d) => d.quicHost) || devices[0] || null;
184
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaver-feedback-react-native",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Visual feedback SDK for Yaver — bug reports, screen recording, voice annotations, and local-first developer workflows",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",