@peers-app/peers-ui 0.21.2 → 0.21.4

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.
Files changed (31) hide show
  1. package/dist/screens/network-viewer/connection-troubleshooter.js +1 -1
  2. package/dist/screens/network-viewer/device-details-modal.js +11 -15
  3. package/dist/screens/network-viewer/group-details-modal.js +60 -17
  4. package/dist/screens/network-viewer/network-viewer-ipc.d.ts +9 -39
  5. package/dist/screens/network-viewer/network-viewer-ipc.js +71 -3
  6. package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +1 -0
  7. package/dist/screens/network-viewer/network-viewer-ipc.test.js +59 -0
  8. package/dist/screens/network-viewer/network-viewer.js +46 -60
  9. package/dist/screens/network-viewer/remote-logs-panel.js +11 -12
  10. package/dist/tabs-layout/network-status-indicator.d.ts +41 -0
  11. package/dist/tabs-layout/network-status-indicator.js +261 -0
  12. package/dist/tabs-layout/network-status-indicator.test.d.ts +1 -0
  13. package/dist/tabs-layout/network-status-indicator.test.js +333 -0
  14. package/dist/tabs-layout/tabs-layout.js +25 -24
  15. package/dist/tabs-layout/use-network-status.d.ts +8 -0
  16. package/dist/tabs-layout/use-network-status.js +57 -0
  17. package/dist/tabs-layout/use-network-status.test.d.ts +1 -0
  18. package/dist/tabs-layout/use-network-status.test.js +42 -0
  19. package/package.json +3 -3
  20. package/src/screens/network-viewer/connection-troubleshooter.tsx +2 -2
  21. package/src/screens/network-viewer/device-details-modal.tsx +19 -61
  22. package/src/screens/network-viewer/group-details-modal.tsx +211 -130
  23. package/src/screens/network-viewer/network-viewer-ipc.test.ts +70 -0
  24. package/src/screens/network-viewer/network-viewer-ipc.ts +102 -35
  25. package/src/screens/network-viewer/network-viewer.tsx +76 -148
  26. package/src/screens/network-viewer/remote-logs-panel.tsx +14 -16
  27. package/src/tabs-layout/network-status-indicator.test.tsx +391 -0
  28. package/src/tabs-layout/network-status-indicator.tsx +475 -0
  29. package/src/tabs-layout/tabs-layout.tsx +4 -1
  30. package/src/tabs-layout/use-network-status.test.tsx +55 -0
  31. package/src/tabs-layout/use-network-status.ts +68 -0
@@ -1,8 +1,8 @@
1
- import { TrustLevel } from "@peers-app/peers-sdk";
1
+ import { type INetworkDeviceDetails, TrustLevel } from "@peers-app/peers-sdk";
2
2
  import { useCallback, useEffect, useState } from "react";
3
3
  import { LoadingIndicator } from "../../components/loading-indicator";
4
4
  import { TrustLevelBadge } from "../../components/trust-level-badge";
5
- import { getNetworkViewerApi } from "./network-viewer-ipc";
5
+ import { getNetworkControlConsumer, getNetworkDiagnosticsConsumer } from "./network-viewer-ipc";
6
6
  import { RemoteLogsPanel } from "./remote-logs-panel";
7
7
 
8
8
  /** Format bytes to human-readable string (KB, MB, GB) */
@@ -24,41 +24,6 @@ function formatRate(mbps: number | undefined): string {
24
24
  return `${mbps.toFixed(2)} MB/s`;
25
25
  }
26
26
 
27
- // TypeScript type definitions (matching backend)
28
- interface IDeviceDetails {
29
- deviceId: string;
30
- userId: string;
31
- userName?: string;
32
- deviceName?: string;
33
- trustLevel: number;
34
- connectionState: "connected" | "syncing" | "idle";
35
- canReadRemoteLogs: boolean;
36
- sharedGroups: string[];
37
- roles: Record<string, unknown>;
38
- pathType: "direct" | "indirect" | "unknown";
39
- throughDeviceIds?: string[];
40
- networkInfo?: {
41
- deviceId: string;
42
- connections: Array<{ deviceId: string; userId: string }>;
43
- preferredDeviceIds: string[];
44
- connectionSlotsAvailable: number;
45
- cpuPercent: number;
46
- memPercent: number;
47
- timestampLastApplied: number;
48
- };
49
- syncStatsByGroup: {
50
- [groupId: string]: {
51
- lastSyncTime: number;
52
- isPreferred: boolean;
53
- };
54
- };
55
- // Throughput stats
56
- bytesSent?: number;
57
- bytesReceived?: number;
58
- sendRateMBps?: number;
59
- receiveRateMBps?: number;
60
- }
61
-
62
27
  interface IDeviceDetailsModalProps {
63
28
  deviceId: string;
64
29
  onClose: () => void;
@@ -67,22 +32,14 @@ interface IDeviceDetailsModalProps {
67
32
 
68
33
  /** Show connection, sync, topology, and provider-authorized remote-log diagnostics for one device. */
69
34
  export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceDetailsModalProps) {
70
- const [device, setDevice] = useState<IDeviceDetails | null>(null);
35
+ const [device, setDevice] = useState<INetworkDeviceDetails | null>(null);
71
36
  const [loading, setLoading] = useState(true);
72
37
  const [notFound, setNotFound] = useState(false);
73
38
 
74
39
  const loadData = useCallback(async () => {
75
40
  try {
76
- const api = getNetworkViewerApi();
77
- if (!api) {
78
- console.warn("Network Viewer API not available");
79
- return;
80
- }
81
-
82
- const deviceData = (await api.getDeviceDetails(deviceId)) as
83
- | IDeviceDetails
84
- | null
85
- | undefined;
41
+ const diagnostics = await getNetworkDiagnosticsConsumer();
42
+ const { device: deviceData } = await diagnostics.tools.getDeviceDetails({ deviceId });
86
43
 
87
44
  if (!deviceData) {
88
45
  setNotFound(true);
@@ -104,14 +61,14 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
104
61
  }
105
62
 
106
63
  try {
107
- const api = getNetworkViewerApi();
108
- if (api && deviceId) {
109
- await api.disconnectDevice(deviceId);
110
- if (onDisconnect) {
111
- onDisconnect();
112
- }
113
- onClose();
64
+ const control = await getNetworkControlConsumer();
65
+ const result = await control.tools.disconnectDevice({ deviceId });
66
+ if (!result.success) {
67
+ alert(result.message);
68
+ return;
114
69
  }
70
+ onDisconnect?.();
71
+ onClose();
115
72
  } catch (error) {
116
73
  console.error("Error disconnecting device:", error);
117
74
  }
@@ -197,9 +154,14 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
197
154
  <span className="badge bg-success">{device.connectionState}</span>
198
155
  </div>
199
156
  <div className="col-md-6 mb-3">
200
- <strong>Trust Level:</strong>
157
+ <strong>User Trust:</strong>
201
158
  <br />
202
- <TrustLevelBadge level={device.trustLevel || TrustLevel.Unknown} />
159
+ <TrustLevelBadge level={device.userTrustLevel || TrustLevel.Unknown} />
160
+ </div>
161
+ <div className="col-md-6 mb-3">
162
+ <strong>Device Trust:</strong>
163
+ <br />
164
+ <TrustLevelBadge level={device.deviceTrustLevel || TrustLevel.Unknown} />
203
165
  </div>
204
166
  <div className="col-md-6 mb-3">
205
167
  <strong>Path Type:</strong>
@@ -345,7 +307,6 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
345
307
  <thead>
346
308
  <tr>
347
309
  <th>Device ID</th>
348
- <th>User ID</th>
349
310
  </tr>
350
311
  </thead>
351
312
  <tbody>
@@ -354,9 +315,6 @@ export function DeviceDetailsModal({ deviceId, onClose, onDisconnect }: IDeviceD
354
315
  <td>
355
316
  <code className="text-muted small">{conn.deviceId}</code>
356
317
  </td>
357
- <td>
358
- <code className="text-muted small">{conn.userId}</code>
359
- </td>
360
318
  </tr>
361
319
  ))}
362
320
  </tbody>
@@ -1,38 +1,16 @@
1
- import { TrustLevel } from "@peers-app/peers-sdk";
1
+ import {
2
+ type INetworkGroupDetails,
3
+ type INetworkGroupSyncInfo,
4
+ TrustLevel,
5
+ } from "@peers-app/peers-sdk";
2
6
  import { useCallback, useEffect, useState } from "react";
3
7
  import { LoadingIndicator } from "../../components/loading-indicator";
4
8
  import { TrustLevelBadge } from "../../components/trust-level-badge";
5
- import { getNetworkViewerApi } from "./network-viewer-ipc";
6
-
7
- // TypeScript type definitions (matching backend)
8
- interface IGroupDetails {
9
- groupId: string;
10
- groupName: string;
11
- founderUserId: string;
12
- founderUserName?: string;
13
- members: {
14
- userId: string;
15
- userName?: string;
16
- role: string;
17
- trustLevel: TrustLevel;
18
- }[];
19
- }
20
-
21
- interface IConnectedDevice {
22
- deviceId: string;
23
- latencyMs: number;
24
- errorRate: number;
25
- timestampLastApplied: number;
26
- }
27
-
28
- interface IGroupSyncInfo {
29
- groupId: string;
30
- groupName: string;
31
- connectedDevices: IConnectedDevice[];
32
- preferredDeviceIds: string[];
33
- lastSyncTimestamp: number;
34
- totalMembers: number;
35
- }
9
+ import {
10
+ getNetworkControlConsumer,
11
+ getNetworkDiagnosticsConsumer,
12
+ getNetworkViewerPlatformApi,
13
+ } from "./network-viewer-ipc";
36
14
 
37
15
  interface IGroupDetailsModalProps {
38
16
  groupId: string;
@@ -40,8 +18,8 @@ interface IGroupDetailsModalProps {
40
18
  }
41
19
 
42
20
  export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps) {
43
- const [group, setGroup] = useState<IGroupDetails | null>(null);
44
- const [syncInfo, setSyncInfo] = useState<IGroupSyncInfo | null>(null);
21
+ const [group, setGroup] = useState<INetworkGroupDetails | null>(null);
22
+ const [syncInfo, setSyncInfo] = useState<INetworkGroupSyncInfo | null>(null);
45
23
  const [loading, setLoading] = useState(true);
46
24
  const [notFound, setNotFound] = useState(false);
47
25
  const [downloading, setDownloading] = useState(false);
@@ -52,21 +30,26 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
52
30
  filePath?: string;
53
31
  size?: number;
54
32
  } | null>(null);
33
+ const [fullSyncing, setFullSyncing] = useState(false);
34
+ const [fullSyncResult, setFullSyncResult] = useState<{
35
+ success: boolean;
36
+ message: string;
37
+ } | null>(null);
38
+ const [syncingNow, setSyncingNow] = useState(false);
39
+ const [syncNowResult, setSyncNowResult] = useState<{
40
+ success: boolean;
41
+ message: string;
42
+ } | null>(null);
43
+ const platformApi = getNetworkViewerPlatformApi();
55
44
 
56
45
  const loadData = useCallback(async () => {
57
46
  try {
58
- const api = getNetworkViewerApi();
59
- if (!api) {
60
- console.warn("Network Viewer API not available");
61
- return;
62
- }
63
-
64
- const [groupData, syncDataUnknown] = await Promise.all([
65
- api.getGroupDetails(groupId),
66
- api.getGroupSyncStatus(groupId),
47
+ const diagnostics = await getNetworkDiagnosticsConsumer();
48
+ const [{ group: groupDetails }, { groups }] = await Promise.all([
49
+ diagnostics.tools.getGroupDetails({ groupId }),
50
+ diagnostics.tools.getGroupSyncStatus({ groupId }),
67
51
  ]);
68
52
 
69
- const groupDetails = groupData as IGroupDetails | null | undefined;
70
53
  if (!groupDetails) {
71
54
  setNotFound(true);
72
55
  } else {
@@ -74,9 +57,8 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
74
57
  setNotFound(false);
75
58
  }
76
59
 
77
- const syncData = syncDataUnknown as IGroupSyncInfo[] | undefined;
78
- if (syncData && syncData.length > 0) {
79
- setSyncInfo(syncData[0]);
60
+ if (groups.length > 0) {
61
+ setSyncInfo(groups[0]);
80
62
  }
81
63
  } catch (error) {
82
64
  console.error("Error loading group details:", error);
@@ -92,8 +74,7 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
92
74
  setDownloadProgress("Preparing download...");
93
75
  setDownloadResult(null);
94
76
 
95
- const api = getNetworkViewerApi();
96
- if (!api?.downloadRemoteDatabase) {
77
+ if (!platformApi?.downloadRemoteDatabase) {
97
78
  setDownloadResult({
98
79
  success: false,
99
80
  message: "Download API not available",
@@ -102,7 +83,7 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
102
83
  }
103
84
 
104
85
  setDownloadProgress("Downloading database...");
105
- const result = (await api.downloadRemoteDatabase(groupId, deviceId)) as {
86
+ const result = (await platformApi.downloadRemoteDatabase(groupId, deviceId)) as {
106
87
  success: boolean;
107
88
  message: string;
108
89
  filePath?: string;
@@ -125,6 +106,53 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
125
106
  }
126
107
  };
127
108
 
109
+ const handleSyncNow = async () => {
110
+ setSyncingNow(true);
111
+ setSyncNowResult(null);
112
+ try {
113
+ const control = await getNetworkControlConsumer();
114
+ const result = await control.tools.syncGroupNow({ groupId });
115
+ setSyncNowResult(result);
116
+ if (result.success) {
117
+ await loadData();
118
+ }
119
+ } catch (error) {
120
+ setSyncNowResult({
121
+ success: false,
122
+ message: error instanceof Error ? error.message : String(error),
123
+ });
124
+ } finally {
125
+ setSyncingNow(false);
126
+ }
127
+ };
128
+
129
+ const handleFullSync = async () => {
130
+ if (
131
+ !confirm(
132
+ `Do a full sync for "${group?.groupName || groupId}"? This resets local change tracking for only this context and may transfer substantial data.`,
133
+ )
134
+ ) {
135
+ return;
136
+ }
137
+ setFullSyncing(true);
138
+ setFullSyncResult(null);
139
+ try {
140
+ const control = await getNetworkControlConsumer();
141
+ const result = await control.tools.resetChangeTracking({ dataContextId: groupId });
142
+ setFullSyncResult(result);
143
+ if (result.success) {
144
+ await loadData();
145
+ }
146
+ } catch (error) {
147
+ setFullSyncResult({
148
+ success: false,
149
+ message: error instanceof Error ? error.message : String(error),
150
+ });
151
+ } finally {
152
+ setFullSyncing(false);
153
+ }
154
+ };
155
+
128
156
  useEffect(() => {
129
157
  loadData();
130
158
  }, [loadData]);
@@ -239,98 +267,151 @@ export function GroupDetailsModal({ groupId, onClose }: IGroupDetailsModalProps)
239
267
  </div>
240
268
  )}
241
269
 
242
- {/* Database Download Section */}
243
270
  <div className="mb-4">
244
271
  <h6 className="border-bottom pb-2 mb-3">
245
- <i className="bi bi-download me-2"></i>
246
- Download Database (Fast Sync)
272
+ <i className="bi bi-arrow-repeat me-2"></i>
273
+ Synchronization
247
274
  </h6>
248
275
  <p className="text-muted small mb-3">
249
- Download the entire database from a connected device. This is useful for fast
250
- initial sync on a new device. The database will be saved as a backup file.
276
+ <strong>Sync now</strong> notifies connected peers that this context may have
277
+ changes. <strong>Do full sync</strong> resets change tracking for this context and
278
+ rebuilds synchronization state; that may transfer substantial data and should be
279
+ used only to repair suspected drift.
251
280
  </p>
252
-
253
- {syncInfo && syncInfo.connectedDevices.length > 0 ? (
254
- <div>
255
- <label className="form-label">Select a connected device:</label>
256
- <div className="d-flex flex-wrap gap-2">
257
- {syncInfo.connectedDevices.map((device) => (
258
- <button
259
- key={device.deviceId}
260
- className="btn btn-outline-primary btn-sm"
261
- onClick={() => handleDownloadDatabase(device.deviceId)}
262
- disabled={downloading}
263
- >
264
- {downloading ? (
265
- <>
266
- <span
267
- className="spinner-border spinner-border-sm me-1"
268
- role="status"
269
- aria-hidden="true"
270
- ></span>
271
- Downloading...
272
- </>
273
- ) : (
274
- <>
275
- <i className="bi bi-cloud-download me-1"></i>
276
- {device.deviceId.substring(0, 8)}...
277
- <span className="badge bg-secondary ms-1">
278
- {Math.round(device.latencyMs)}ms
279
- </span>
280
- </>
281
- )}
282
- </button>
283
- ))}
284
- </div>
285
- </div>
286
- ) : (
287
- <div className="alert alert-info mb-0">
288
- <i className="bi bi-info-circle me-2"></i>
289
- No devices are currently connected to this group. Connect to a device first to
290
- download its database.
291
- </div>
292
- )}
293
-
294
- {downloadProgress && (
295
- <div className="mt-3">
296
- <div className="d-flex align-items-center">
297
- <span
298
- className="spinner-border spinner-border-sm me-2"
299
- role="status"
300
- aria-hidden="true"
301
- ></span>
302
- <span>{downloadProgress}</span>
303
- </div>
281
+ <div className="d-flex flex-wrap gap-2">
282
+ <button
283
+ type="button"
284
+ className="btn btn-sm btn-outline-primary"
285
+ onClick={() => void handleSyncNow()}
286
+ disabled={syncingNow || fullSyncing}
287
+ >
288
+ {syncingNow ? "Sending sync notification..." : "Sync now"}
289
+ </button>
290
+ <button
291
+ type="button"
292
+ className="btn btn-sm btn-outline-warning"
293
+ onClick={() => void handleFullSync()}
294
+ disabled={fullSyncing || syncingNow}
295
+ >
296
+ {fullSyncing ? "Starting full sync..." : "Do full sync"}
297
+ </button>
298
+ </div>
299
+ {syncNowResult && (
300
+ <div
301
+ className={`alert mt-3 mb-0 ${syncNowResult.success ? "alert-success" : "alert-danger"}`}
302
+ role="status"
303
+ >
304
+ {syncNowResult.message}
304
305
  </div>
305
306
  )}
306
-
307
- {downloadResult && (
307
+ {fullSyncResult && (
308
308
  <div
309
- className={`alert mt-3 mb-0 ${downloadResult.success ? "alert-success" : "alert-danger"}`}
309
+ className={`alert mt-3 mb-0 ${fullSyncResult.success ? "alert-success" : "alert-danger"}`}
310
+ role="status"
310
311
  >
311
- {downloadResult.success ? (
312
- <>
313
- <i className="bi bi-check-circle me-2"></i>
314
- <strong>Download complete!</strong>
315
- <br />
316
- <small>
317
- File: <code>{downloadResult.filePath}</code>
318
- <br />
319
- Size:{" "}
320
- {downloadResult.size
321
- ? `${(downloadResult.size / (1024 * 1024)).toFixed(2)} MB`
322
- : "Unknown"}
323
- </small>
324
- </>
325
- ) : (
326
- <>
327
- <i className="bi bi-exclamation-circle me-2"></i>
328
- <strong>Download failed:</strong> {downloadResult.message}
329
- </>
330
- )}
312
+ {fullSyncResult.message}
331
313
  </div>
332
314
  )}
333
315
  </div>
316
+
317
+ {/* Database Download Section */}
318
+ {platformApi?.downloadRemoteDatabase ? (
319
+ <div className="mb-4">
320
+ <h6 className="border-bottom pb-2 mb-3">
321
+ <i className="bi bi-download me-2"></i>
322
+ Download Database (Fast Sync)
323
+ </h6>
324
+ <p className="text-muted small mb-3">
325
+ Download the entire database from a connected device. This is useful for fast
326
+ initial sync on a new device. The database will be saved as a backup file.
327
+ </p>
328
+
329
+ {syncInfo && syncInfo.connectedDevices.length > 0 ? (
330
+ <div>
331
+ <label className="form-label">Select a connected device:</label>
332
+ <div className="d-flex flex-wrap gap-2">
333
+ {syncInfo.connectedDevices.map((device) => (
334
+ <button
335
+ key={device.deviceId}
336
+ className="btn btn-outline-primary btn-sm"
337
+ onClick={() => handleDownloadDatabase(device.deviceId)}
338
+ disabled={downloading}
339
+ >
340
+ {downloading ? (
341
+ <>
342
+ <span
343
+ className="spinner-border spinner-border-sm me-1"
344
+ role="status"
345
+ aria-hidden="true"
346
+ ></span>
347
+ Downloading...
348
+ </>
349
+ ) : (
350
+ <>
351
+ <i className="bi bi-cloud-download me-1"></i>
352
+ {device.deviceId.substring(0, 8)}...
353
+ <span className="badge bg-secondary ms-1">
354
+ {Math.round(device.latencyMs)}ms
355
+ </span>
356
+ </>
357
+ )}
358
+ </button>
359
+ ))}
360
+ </div>
361
+ </div>
362
+ ) : (
363
+ <div className="alert alert-info mb-0">
364
+ <i className="bi bi-info-circle me-2"></i>
365
+ No devices are currently connected to this group. Connect to a device first to
366
+ download its database.
367
+ </div>
368
+ )}
369
+
370
+ {downloadProgress && (
371
+ <div className="mt-3">
372
+ <div className="d-flex align-items-center">
373
+ <span
374
+ className="spinner-border spinner-border-sm me-2"
375
+ role="status"
376
+ aria-hidden="true"
377
+ ></span>
378
+ <span>{downloadProgress}</span>
379
+ </div>
380
+ </div>
381
+ )}
382
+
383
+ {downloadResult && (
384
+ <div
385
+ className={`alert mt-3 mb-0 ${downloadResult.success ? "alert-success" : "alert-danger"}`}
386
+ >
387
+ {downloadResult.success ? (
388
+ <>
389
+ <i className="bi bi-check-circle me-2"></i>
390
+ <strong>Download complete!</strong>
391
+ <br />
392
+ <small>
393
+ File: <code>{downloadResult.filePath}</code>
394
+ <br />
395
+ Size:{" "}
396
+ {downloadResult.size
397
+ ? `${(downloadResult.size / (1024 * 1024)).toFixed(2)} MB`
398
+ : "Unknown"}
399
+ </small>
400
+ </>
401
+ ) : (
402
+ <>
403
+ <i className="bi bi-exclamation-circle me-2"></i>
404
+ <strong>Download failed:</strong> {downloadResult.message}
405
+ </>
406
+ )}
407
+ </div>
408
+ )}
409
+ </div>
410
+ ) : (
411
+ <div className="alert alert-info mb-4">
412
+ Raw database download is available only in the Electron desktop app.
413
+ </div>
414
+ )}
334
415
  </>
335
416
  )}
336
417
  </div>
@@ -0,0 +1,70 @@
1
+ import {
2
+ CONTRACT_REQUEST_CHANNEL,
3
+ inProcessTransportPair,
4
+ newid,
5
+ registerHostContractTransport,
6
+ } from "@peers-app/peers-sdk";
7
+ import { getNetworkControlConsumer, getNetworkDiagnosticsConsumer } from "./network-viewer-ipc";
8
+
9
+ function setupHost(label: string) {
10
+ const [consumerTransport, providerTransport] = inProcessTransportPair();
11
+ providerTransport.on(CONTRACT_REQUEST_CHANNEL, (call) => {
12
+ if (call.member === "observable" && call.name === "status" && call.operation === "get") {
13
+ return {
14
+ state: "offline",
15
+ connectionCount: 0,
16
+ syncingContextCount: 0,
17
+ errorCount: 0,
18
+ errors: [],
19
+ };
20
+ }
21
+ if (call.member === "tool" && call.name === "getOverview") {
22
+ return { overview: { localDeviceId: label } };
23
+ }
24
+ if (call.member === "tool" && call.name === "refreshNetworkInfo") {
25
+ return { success: true, message: label };
26
+ }
27
+ return undefined;
28
+ });
29
+ return {
30
+ dispose: registerHostContractTransport(consumerTransport),
31
+ };
32
+ }
33
+
34
+ describe("Network Viewer host consumers", () => {
35
+ it("caches consumers for one transport and recreates them after replacement", async () => {
36
+ const firstHost = setupHost(newid());
37
+ const firstDiagnostics = await getNetworkDiagnosticsConsumer();
38
+ expect(await getNetworkDiagnosticsConsumer()).toBe(firstDiagnostics);
39
+
40
+ const secondLabel = newid();
41
+ const secondHost = setupHost(secondLabel);
42
+ const secondDiagnostics = await getNetworkDiagnosticsConsumer();
43
+ const control = await getNetworkControlConsumer();
44
+
45
+ expect(secondDiagnostics).not.toBe(firstDiagnostics);
46
+ await expect(firstDiagnostics.tools.getOverview()).rejects.toThrow("disposed");
47
+ await expect(control.tools.refreshNetworkInfo()).resolves.toEqual({
48
+ success: true,
49
+ message: secondLabel,
50
+ });
51
+
52
+ secondHost.dispose();
53
+ firstHost.dispose();
54
+ });
55
+
56
+ it("shares one consumer pair across concurrent callers", async () => {
57
+ const host = setupHost(newid());
58
+ const [first, second, third] = await Promise.all([
59
+ getNetworkDiagnosticsConsumer(),
60
+ getNetworkDiagnosticsConsumer(),
61
+ getNetworkControlConsumer(),
62
+ ]);
63
+
64
+ expect(second).toBe(first);
65
+ const cachedControl = await getNetworkControlConsumer();
66
+ expect(third).toBe(cachedControl);
67
+
68
+ host.dispose();
69
+ });
70
+ });