@opengeni/react 0.52.1 → 0.57.0

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 (132) hide show
  1. package/README.md +48 -0
  2. package/dist/artifacts.d.ts +1 -1
  3. package/dist/artifacts.js +43 -14
  4. package/dist/artifacts.js.map +1 -1
  5. package/dist/{chunk-ROIRTM37.js → chunk-5UQPNLIO.js} +6 -6
  6. package/dist/{chunk-JR3QFY6B.js → chunk-6KLX2RH5.js} +49 -2
  7. package/dist/chunk-6KLX2RH5.js.map +1 -0
  8. package/dist/{chunk-FWS3KC3Z.js → chunk-B6WPZSTT.js} +104 -104
  9. package/dist/{chunk-FWS3KC3Z.js.map → chunk-B6WPZSTT.js.map} +1 -1
  10. package/dist/{chunk-HDBG7XGO.js → chunk-C5ZPSSS5.js} +50 -1
  11. package/dist/chunk-C5ZPSSS5.js.map +1 -0
  12. package/dist/{chunk-X4S56Y2H.js → chunk-HA46CYQK.js} +135 -11
  13. package/dist/chunk-HA46CYQK.js.map +1 -0
  14. package/dist/{chunk-724BW4TW.js → chunk-IGC6A2ZI.js} +2 -2
  15. package/dist/chunk-LLOLUHVG.js +5937 -0
  16. package/dist/chunk-LLOLUHVG.js.map +1 -0
  17. package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
  18. package/dist/chunk-MKPIAGVL.js.map +1 -0
  19. package/dist/chunk-R3AZOAB5.js +933 -0
  20. package/dist/chunk-R3AZOAB5.js.map +1 -0
  21. package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
  22. package/dist/chunk-XBZWOM2K.js.map +1 -0
  23. package/dist/{chunk-JAGDUCZQ.js → chunk-Y7OEDUJ5.js} +17 -13
  24. package/dist/chunk-Y7OEDUJ5.js.map +1 -0
  25. package/dist/client.d.ts +6 -4
  26. package/dist/components/artifacts/index.d.ts +1 -0
  27. package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
  28. package/dist/components/browser-input.d.ts +7 -0
  29. package/dist/components/browser-viewer.d.ts +5 -4
  30. package/dist/components/computer-viewer.d.ts +2 -1
  31. package/dist/components/desktop-viewer.d.ts +26 -2
  32. package/dist/components/interaction-intervention-banner.d.ts +11 -0
  33. package/dist/components/sandbox-terminal.d.ts +4 -1
  34. package/dist/components/sandbox-workspace.d.ts +3 -0
  35. package/dist/composer.js +6 -6
  36. package/dist/hooks/use-auth-runs.d.ts +32 -0
  37. package/dist/hooks/use-browser-downloads.d.ts +25 -0
  38. package/dist/hooks/use-browser-identities.d.ts +5 -1
  39. package/dist/hooks/use-browser-session.d.ts +2 -1
  40. package/dist/hooks/use-browser-sessions.d.ts +1 -1
  41. package/dist/hooks/use-channels.d.ts +26 -0
  42. package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
  43. package/dist/hooks/use-computer-session.d.ts +2 -1
  44. package/dist/hooks/use-computer-sessions.d.ts +1 -1
  45. package/dist/hooks/use-desktop-stream.d.ts +4 -0
  46. package/dist/hooks/use-interaction-interventions.d.ts +27 -0
  47. package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
  48. package/dist/hooks/use-network-routes.d.ts +25 -0
  49. package/dist/hooks/use-site-auth-connections.d.ts +25 -0
  50. package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
  51. package/dist/hooks/use-terminal-stream.d.ts +3 -0
  52. package/dist/index.d.ts +4 -0
  53. package/dist/index.js +1220 -1545
  54. package/dist/index.js.map +1 -1
  55. package/dist/interaction.d.ts +13 -3
  56. package/dist/interaction.js +520 -6
  57. package/dist/interaction.js.map +1 -1
  58. package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
  59. package/dist/lib/host-platform.d.ts +5 -0
  60. package/dist/lib/relay-wire.d.ts +20 -1
  61. package/dist/machines.js +3 -3
  62. package/dist/model-policy.d.ts +1 -1
  63. package/dist/model-policy.js +1 -1
  64. package/dist/provider.d.ts +3 -2
  65. package/dist/realtime/realtime-control.d.ts +2 -2
  66. package/dist/realtime.js +36 -9
  67. package/dist/realtime.js.map +1 -1
  68. package/dist/session-context.d.ts +10 -2
  69. package/dist/session-ui.js +3 -3
  70. package/dist/session.js +11 -11
  71. package/dist/timeline/types.d.ts +5 -0
  72. package/package.json +2 -2
  73. package/src/artifacts.ts +3 -0
  74. package/src/client.ts +73 -3
  75. package/src/components/artifacts/index.ts +5 -0
  76. package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
  77. package/src/components/browser-input.ts +65 -0
  78. package/src/components/browser-viewer.tsx +1287 -177
  79. package/src/components/computer-viewer.tsx +611 -114
  80. package/src/components/desktop-viewer.tsx +114 -1
  81. package/src/components/interaction-intervention-banner.tsx +99 -0
  82. package/src/components/machines/machine-detail.tsx +1 -1
  83. package/src/components/model-policy-picker.tsx +15 -0
  84. package/src/components/sandbox-terminal.tsx +24 -7
  85. package/src/components/sandbox-workspace.tsx +42 -4
  86. package/src/components/workspace-dock.tsx +59 -5
  87. package/src/hooks/use-attached-browsers.ts +13 -11
  88. package/src/hooks/use-auth-runs.ts +278 -0
  89. package/src/hooks/use-browser-downloads.ts +218 -0
  90. package/src/hooks/use-browser-frame-stream.ts +168 -59
  91. package/src/hooks/use-browser-identities.ts +47 -6
  92. package/src/hooks/use-browser-session.ts +94 -27
  93. package/src/hooks/use-browser-sessions.ts +19 -14
  94. package/src/hooks/use-channels.ts +99 -0
  95. package/src/hooks/use-computer-frame-stream.ts +184 -61
  96. package/src/hooks/use-computer-session.ts +22 -2
  97. package/src/hooks/use-computer-sessions.ts +19 -13
  98. package/src/hooks/use-desktop-stream.ts +39 -7
  99. package/src/hooks/use-interaction-interventions.ts +242 -0
  100. package/src/hooks/use-interaction-invalidation.ts +66 -0
  101. package/src/hooks/use-machine-chip.ts +13 -3
  102. package/src/hooks/use-network-routes.ts +214 -0
  103. package/src/hooks/use-site-auth-connections.ts +226 -0
  104. package/src/hooks/use-supergrok-accounts.ts +81 -0
  105. package/src/hooks/use-terminal-stream.ts +288 -55
  106. package/src/index.ts +8 -0
  107. package/src/interaction.ts +30 -0
  108. package/src/lib/host-platform.ts +10 -0
  109. package/src/lib/relay-wire.ts +43 -4
  110. package/src/lib/terminal-capability.ts +1 -1
  111. package/src/model-policy.ts +19 -4
  112. package/src/provider.tsx +133 -1
  113. package/src/realtime/realtime-control.tsx +34 -6
  114. package/src/session-context.ts +66 -1
  115. package/src/timeline/activity-rail.tsx +6 -0
  116. package/src/timeline/projection.ts +58 -0
  117. package/src/timeline/types.ts +2 -0
  118. package/styles/compiled.css +64 -6
  119. package/dist/chunk-23EJ676W.js.map +0 -1
  120. package/dist/chunk-6BBWWOOU.js +0 -82
  121. package/dist/chunk-6BBWWOOU.js.map +0 -1
  122. package/dist/chunk-7I2KB3OK.js +0 -3895
  123. package/dist/chunk-7I2KB3OK.js.map +0 -1
  124. package/dist/chunk-HDBG7XGO.js.map +0 -1
  125. package/dist/chunk-JAGDUCZQ.js.map +0 -1
  126. package/dist/chunk-JR3QFY6B.js.map +0 -1
  127. package/dist/chunk-WYA65Y3F.js.map +0 -1
  128. package/dist/chunk-X4S56Y2H.js.map +0 -1
  129. package/dist/interactive-workbench-viewers-KHNOHWQE.js +0 -13
  130. /package/dist/{chunk-ROIRTM37.js.map → chunk-5UQPNLIO.js.map} +0 -0
  131. /package/dist/{chunk-724BW4TW.js.map → chunk-IGC6A2ZI.js.map} +0 -0
  132. /package/dist/{interactive-workbench-viewers-KHNOHWQE.js.map → interactive-workbench-viewers-TOHBXJEE.js.map} +0 -0
@@ -13,7 +13,9 @@ import {
13
13
  import {
14
14
  decodeStreamFrame,
15
15
  decodeStreamOpenAck,
16
+ encodeStreamClose,
16
17
  encodeStreamOpen,
18
+ STREAM_CLOSE_REASON_NORMAL,
17
19
  STREAM_KIND_BROWSER,
18
20
  STREAM_ROLE_CLIENT,
19
21
  } from "../lib/relay-wire";
@@ -39,6 +41,8 @@ export type BrowserFrameWebSocketFactory = (
39
41
  const RELAY_TAG_OPEN = 1;
40
42
  const RELAY_TAG_OPEN_ACK = 2;
41
43
  const RELAY_TAG_FRAME = 3;
44
+ const RELAY_TAG_CLOSE = 4;
45
+ const MAX_AUTOMATIC_RECONNECTS_WITHOUT_A_FRAME = 2;
42
46
 
43
47
  export type UseBrowserFrameStreamOptions = EmbeddedBrowserInteractionClientOverride & {
44
48
  browserSessionId: string | null;
@@ -108,48 +112,81 @@ export function useBrowserFrameStream(
108
112
 
109
113
  let disposed = false;
110
114
  let socket: BrowserFrameWebSocket | null = null;
115
+ let socketHandlers: {
116
+ open: () => void;
117
+ message: (event: MessageEvent) => void;
118
+ error: () => void;
119
+ close: () => void;
120
+ } | null = null;
111
121
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
112
122
  let expiryTimer: ReturnType<typeof setTimeout> | null = null;
113
123
  let failures = 0;
114
124
  let controllerGeneration: string | null = null;
125
+ let activeAttachment: BrowserSessionAttachment | null = null;
115
126
  let activeStream: BrowserSessionAttachment["stream"] | null = null;
127
+ let attachmentExpiresAt = 0;
128
+ let lastRelaySequence: string | null = null;
116
129
  let relayAccepted = false;
130
+ let pendingFrame: { bytes: Uint8Array; source: BrowserFrameWebSocket } | null = null;
131
+ let decodingFrame = false;
117
132
  const attachmentAbort = new AbortController();
118
133
 
119
- const clearSocket = () => {
134
+ const clearSocket = (terminateProducer = false) => {
120
135
  if (!socket) return;
121
- socket.removeEventListener("open", onOpen);
122
- socket.removeEventListener("message", onMessage);
123
- socket.removeEventListener("error", onError);
124
- socket.removeEventListener("close", onClose);
125
- if (socket.readyState === 0 || socket.readyState === 1) {
126
- socket.close(1000, "viewer detached");
136
+ const current = socket;
137
+ const handlers = socketHandlers;
138
+ if (terminateProducer && activeStream?.kind === "relay" && socket.readyState === 1) {
139
+ try {
140
+ socket.send(
141
+ relayDatagram(
142
+ RELAY_TAG_CLOSE,
143
+ encodeStreamClose({
144
+ channelId: activeStream.channel.channelId,
145
+ reason: STREAM_CLOSE_REASON_NORMAL,
146
+ message: "viewer detached",
147
+ }),
148
+ ),
149
+ );
150
+ } catch {
151
+ // OPEN can race a transport close. Local teardown must still finish.
152
+ }
153
+ }
154
+ if (handlers) {
155
+ current.removeEventListener("open", handlers.open);
156
+ current.removeEventListener("message", handlers.message);
157
+ current.removeEventListener("error", handlers.error);
158
+ current.removeEventListener("close", handlers.close);
159
+ }
160
+ if (current.readyState === 0 || current.readyState === 1) {
161
+ current.close(1000, "viewer detached");
127
162
  }
128
163
  socket = null;
164
+ socketHandlers = null;
165
+ pendingFrame = null;
129
166
  };
130
167
 
131
168
  const scheduleReconnect = () => {
132
169
  if (disposed || reconnectTimer) return;
170
+ if (failures >= MAX_AUTOMATIC_RECONNECTS_WITHOUT_A_FRAME) return;
133
171
  const delay = Math.min(5_000, 250 * 2 ** Math.min(failures, 5));
134
172
  failures += 1;
135
173
  setResult((current) => ({ ...current, state: "reconnecting" }));
136
174
  reconnectTimer = setTimeout(() => {
137
175
  reconnectTimer = null;
138
- void connect();
176
+ connectSocket();
139
177
  }, delay);
140
178
  };
141
179
 
142
- const fail = (cause: unknown) => {
180
+ const fail = (cause: unknown, reconnectAutomatically = true) => {
143
181
  if (disposed) return;
144
182
  const error = cause instanceof Error ? cause : new Error(String(cause));
145
183
  setResult((current) => ({ ...current, state: "error", error }));
146
184
  clearSocket();
147
- scheduleReconnect();
185
+ if (reconnectAutomatically) scheduleReconnect();
148
186
  };
149
187
 
150
- const onOpen = () => {
151
- if (disposed) return;
152
- failures = 0;
188
+ const onOpen = (source: BrowserFrameWebSocket) => {
189
+ if (disposed || source !== socket) return;
153
190
  if (activeStream?.kind === "relay") {
154
191
  const body = encodeStreamOpen({
155
192
  channel: {
@@ -158,19 +195,61 @@ export function useBrowserFrameStream(
158
195
  },
159
196
  token: activeStream.token,
160
197
  role: STREAM_ROLE_CLIENT,
161
- resumeFromSeq: "0",
198
+ resumeFromSeq:
199
+ lastRelaySequence === null ? "0" : (BigInt(lastRelaySequence) + 1n).toString(),
162
200
  });
163
- socket?.send(relayDatagram(RELAY_TAG_OPEN, body));
201
+ try {
202
+ source.send(relayDatagram(RELAY_TAG_OPEN, body));
203
+ } catch (error) {
204
+ fail(error);
205
+ }
164
206
  return;
165
207
  }
166
208
  setResult((current) => ({ ...current, state: "live", error: null }));
167
209
  };
168
210
 
169
- const onMessage = (event: MessageEvent) => {
211
+ const drainLatestFrame = async () => {
212
+ if (decodingFrame) return;
213
+ decodingFrame = true;
214
+ try {
215
+ while (pendingFrame) {
216
+ if (disposed) break;
217
+ const pending = pendingFrame;
218
+ pendingFrame = null;
219
+ const { bytes, source } = pending;
220
+ const frame = decodeBrowserFrameMessage(bytes);
221
+ if (disposed || source !== socket) continue;
222
+ if (frame.browserSessionId !== browserSessionId || frame.targetId !== targetId) {
223
+ throw new Error("browser frame belongs to another resource");
224
+ }
225
+ if (!controllerGeneration || frame.controllerGeneration !== controllerGeneration) {
226
+ throw new Error("browser frame belongs to a stale controller");
227
+ }
228
+ const key = `${browserSessionId}:${targetId}:${frame.controllerGeneration}:${frame.targetGeneration}:${frame.documentGeneration}`;
229
+ if (latestRef.current.key === key && frame.sequence <= latestRef.current.sequence)
230
+ continue;
231
+ latestRef.current = { key, sequence: frame.sequence };
232
+ failures = 0;
233
+ setResult((current) => ({
234
+ ...current,
235
+ state: "live",
236
+ frame,
237
+ error: null,
238
+ }));
239
+ }
240
+ } catch (cause) {
241
+ fail(cause);
242
+ } finally {
243
+ decodingFrame = false;
244
+ if (!disposed && pendingFrame) void drainLatestFrame();
245
+ }
246
+ };
247
+
248
+ const onMessage = (source: BrowserFrameWebSocket, event: MessageEvent) => {
170
249
  void (async () => {
171
250
  try {
172
251
  const bytes = await messageBytes(event.data);
173
- if (disposed) return;
252
+ if (disposed || source !== socket) return;
174
253
  let frameBytes = bytes;
175
254
  if (activeStream?.kind === "relay") {
176
255
  if (bytes.length < 1) throw new Error("browser relay returned an empty message");
@@ -179,45 +258,83 @@ export function useBrowserFrameStream(
179
258
  if (tag === RELAY_TAG_OPEN_ACK) {
180
259
  const ack = decodeStreamOpenAck(body);
181
260
  if (!ack.accepted) {
261
+ activeAttachment = null;
262
+ activeStream = null;
263
+ attachmentExpiresAt = 0;
182
264
  throw new Error(ack.error?.message ?? "browser stream was rejected by the relay");
183
265
  }
184
266
  relayAccepted = true;
185
267
  return;
186
268
  }
269
+ if (tag === RELAY_TAG_CLOSE) {
270
+ activeAttachment = null;
271
+ activeStream = null;
272
+ attachmentExpiresAt = 0;
273
+ lastRelaySequence = null;
274
+ relayAccepted = false;
275
+ fail(new Error("Browser frame source ended."), false);
276
+ return;
277
+ }
187
278
  if (tag !== RELAY_TAG_FRAME || !relayAccepted) return;
188
- frameBytes = decodeStreamFrame(body).data;
189
- }
190
- const frame = decodeBrowserFrameMessage(frameBytes);
191
- if (frame.browserSessionId !== browserSessionId || frame.targetId !== targetId) {
192
- throw new Error("browser frame belongs to another resource");
193
- }
194
- if (!controllerGeneration || frame.controllerGeneration !== controllerGeneration) {
195
- throw new Error("browser frame belongs to a stale controller");
279
+ const relayFrame = decodeStreamFrame(body);
280
+ lastRelaySequence = relayFrame.seq;
281
+ frameBytes = relayFrame.data;
196
282
  }
197
- const key = `${browserSessionId}:${targetId}:${frame.controllerGeneration}:${frame.targetGeneration}:${frame.documentGeneration}`;
198
- if (latestRef.current.key === key && frame.sequence <= latestRef.current.sequence) return;
199
- latestRef.current = { key, sequence: frame.sequence };
200
- setResult((current) => ({
201
- ...current,
202
- state: "live",
203
- frame,
204
- error: null,
205
- }));
283
+ pendingFrame = { bytes: frameBytes, source };
284
+ void drainLatestFrame();
206
285
  } catch (cause) {
207
286
  fail(cause);
208
287
  }
209
288
  })();
210
289
  };
211
290
 
212
- const onError = () => fail(new Error("Browser view lost connection."));
291
+ const onError = (source: BrowserFrameWebSocket) => {
292
+ if (source !== socket) return;
293
+ fail(new Error("Browser view lost connection."));
294
+ };
213
295
 
214
- const onClose = () => {
215
- if (disposed) return;
296
+ const onClose = (source: BrowserFrameWebSocket) => {
297
+ if (disposed || source !== socket) return;
216
298
  clearSocket();
217
299
  scheduleReconnect();
218
300
  };
219
301
 
220
- const connect = async () => {
302
+ const connectSocket = () => {
303
+ if (disposed) return;
304
+ clearSocket();
305
+ if (!activeAttachment || !activeStream || attachmentExpiresAt <= Date.now() + 1_000) {
306
+ void attach();
307
+ return;
308
+ }
309
+ relayAccepted = false;
310
+ setResult((current) => ({
311
+ ...current,
312
+ state: "connecting",
313
+ error: null,
314
+ }));
315
+ const createSocket =
316
+ factoryRef.current ?? ((url: string, protocols: string[]) => new WebSocket(url, protocols));
317
+ const openedSocket = createSocket(
318
+ activeStream.kind === "relay"
319
+ ? activeStream.url
320
+ : browserFrameSocketUrl(activeAttachment, streamRef.current),
321
+ activeStream.kind === "direct_websocket" ? [...activeStream.protocols] : [],
322
+ );
323
+ socket = openedSocket;
324
+ socketHandlers = {
325
+ open: () => onOpen(openedSocket),
326
+ message: (event) => onMessage(openedSocket, event),
327
+ error: () => onError(openedSocket),
328
+ close: () => onClose(openedSocket),
329
+ };
330
+ openedSocket.binaryType = "arraybuffer";
331
+ openedSocket.addEventListener("open", socketHandlers.open);
332
+ openedSocket.addEventListener("message", socketHandlers.message);
333
+ openedSocket.addEventListener("error", socketHandlers.error);
334
+ openedSocket.addEventListener("close", socketHandlers.close);
335
+ };
336
+
337
+ const attach = async () => {
221
338
  if (disposed) return;
222
339
  clearSocket();
223
340
  if (expiryTimer) clearTimeout(expiryTimer);
@@ -242,7 +359,10 @@ export function useBrowserFrameStream(
242
359
  throw new Error("browser attachment does not match the requested resource");
243
360
  }
244
361
  controllerGeneration = attachment.controllerGeneration;
362
+ activeAttachment = attachment;
245
363
  activeStream = attachment.stream;
364
+ attachmentExpiresAt = Date.parse(attachment.expiresAt);
365
+ lastRelaySequence = null;
246
366
  relayAccepted = false;
247
367
  setResult((current) => ({
248
368
  ...current,
@@ -255,42 +375,31 @@ export function useBrowserFrameStream(
255
375
  },
256
376
  error: null,
257
377
  }));
258
- const createSocket =
259
- factoryRef.current ??
260
- ((url: string, protocols: string[]) => new WebSocket(url, protocols));
261
- socket = createSocket(
262
- attachment.stream.kind === "relay"
263
- ? attachment.stream.url
264
- : browserFrameSocketUrl(attachment, streamRef.current),
265
- attachment.stream.kind === "direct_websocket" ? [...attachment.stream.protocols] : [],
266
- );
267
- socket.binaryType = "arraybuffer";
268
- socket.addEventListener("open", onOpen);
269
- socket.addEventListener("message", onMessage);
270
- socket.addEventListener("error", onError);
271
- socket.addEventListener("close", onClose);
272
- const expiresAt = Date.parse(attachment.expiresAt);
273
- const refreshIn = Number.isFinite(expiresAt)
274
- ? Math.max(1_000, expiresAt - Date.now() - 5_000)
378
+ const refreshIn = Number.isFinite(attachmentExpiresAt)
379
+ ? Math.max(1_000, attachmentExpiresAt - Date.now() - 5_000)
275
380
  : 60_000;
276
381
  expiryTimer = setTimeout(() => {
277
382
  if (disposed) return;
278
- clearSocket();
279
- void connect();
383
+ clearSocket(true);
384
+ activeAttachment = null;
385
+ activeStream = null;
386
+ attachmentExpiresAt = 0;
387
+ void attach();
280
388
  }, refreshIn);
389
+ connectSocket();
281
390
  } catch (cause) {
282
391
  if (attachmentAbort.signal.aborted || disposed) return;
283
392
  fail(cause);
284
393
  }
285
394
  };
286
395
 
287
- void connect();
396
+ void attach();
288
397
  return () => {
289
398
  disposed = true;
290
399
  attachmentAbort.abort();
291
400
  if (reconnectTimer) clearTimeout(reconnectTimer);
292
401
  if (expiryTimer) clearTimeout(expiryTimer);
293
- clearSocket();
402
+ clearSocket(true);
294
403
  };
295
404
  }, [client, enabled, nonce, options.browserSessionId, options.targetId, workspaceId]);
296
405
 
@@ -5,6 +5,7 @@ import type {
5
5
  CreateBrowserIdentityRequest,
6
6
  PublishBrowserRevisionRequest,
7
7
  PublishBrowserRevisionResponse,
8
+ UpdateBrowserIdentityRequest,
8
9
  } from "@opengeni/sdk/interaction";
9
10
  import { useCallback, useEffect, useRef, useState } from "react";
10
11
  import type { EmbeddedBrowserInteractionClientLike } from "../client";
@@ -12,9 +13,11 @@ import {
12
13
  type EmbeddedBrowserInteractionClientOverride,
13
14
  useEmbeddedBrowserInteraction,
14
15
  } from "../session-context";
16
+ import { useInteractionInvalidation } from "./use-interaction-invalidation";
15
17
 
16
18
  export type UseBrowserIdentitiesOptions = EmbeddedBrowserInteractionClientOverride & {
17
19
  enabled?: boolean | undefined;
20
+ includeArchived?: boolean | undefined;
18
21
  };
19
22
 
20
23
  export type UseBrowserIdentitiesResult = {
@@ -30,6 +33,10 @@ export type UseBrowserIdentitiesResult = {
30
33
  operationId?: string;
31
34
  },
32
35
  ) => Promise<BrowserIdentityMutationResponse>;
36
+ update: (
37
+ identityId: string,
38
+ request: Omit<UpdateBrowserIdentityRequest, "operationId"> & { operationId?: string },
39
+ ) => Promise<BrowserIdentityMutationResponse>;
33
40
  revisions: (identityId: string) => Promise<BrowserRevisionListResponse>;
34
41
  publish: (
35
42
  browserSessionId: string,
@@ -43,8 +50,10 @@ export type UseBrowserIdentitiesResult = {
43
50
  export function useBrowserIdentities(
44
51
  options: UseBrowserIdentitiesOptions = {},
45
52
  ): UseBrowserIdentitiesResult {
46
- const { client, workspaceId } = useEmbeddedBrowserInteraction(options);
53
+ const { client, workspaceId, workspaceInteractionEvent, workspaceInteractionConnectionState } =
54
+ useEmbeddedBrowserInteraction(options);
47
55
  const enabled = options.enabled ?? true;
56
+ const includeArchived = options.includeArchived ?? false;
48
57
  const [state, setState] = useState(() => emptyState(workspaceId, enabled));
49
58
  const [mutationCount, setMutationCount] = useState(0);
50
59
  const requestRef = useRef<{ id: number; controller: AbortController | null }>({
@@ -75,6 +84,7 @@ export function useBrowserIdentities(
75
84
  }));
76
85
  try {
77
86
  const response = await client.listBrowserIdentities(workspaceId, {
87
+ includeArchived,
78
88
  signal: controller.signal,
79
89
  });
80
90
  if (!mountedRef.current || requestRef.current.id !== id) return;
@@ -99,7 +109,7 @@ export function useBrowserIdentities(
99
109
  if (requestRef.current.id === id) requestRef.current = { id, controller: null };
100
110
  }
101
111
  },
102
- [client, enabled, workspaceId],
112
+ [client, enabled, includeArchived, workspaceId],
103
113
  );
104
114
 
105
115
  const refresh = useCallback(async () => await load(false), [load]);
@@ -118,6 +128,16 @@ export function useBrowserIdentities(
118
128
  };
119
129
  }, [cancelLoad, enabled, load, workspaceId]);
120
130
 
131
+ useInteractionInvalidation({
132
+ workspaceId,
133
+ key: workspaceId,
134
+ enabled,
135
+ event: workspaceInteractionEvent,
136
+ connectionState: workspaceInteractionConnectionState,
137
+ knownRevision: visibleState.revision,
138
+ refresh,
139
+ });
140
+
121
141
  const mutate = useCallback(async <T>(operation: () => Promise<T>): Promise<T> => {
122
142
  setMutationCount((count) => count + 1);
123
143
  try {
@@ -150,6 +170,24 @@ export function useBrowserIdentities(
150
170
  [client, workspaceId],
151
171
  );
152
172
 
173
+ const update = useCallback(
174
+ async (
175
+ identityId: string,
176
+ request: Omit<UpdateBrowserIdentityRequest, "operationId"> & { operationId?: string },
177
+ ): Promise<BrowserIdentityMutationResponse> =>
178
+ await mutate(async () => {
179
+ const response = await client.updateBrowserIdentity(workspaceId, identityId, {
180
+ ...request,
181
+ operationId: request.operationId ?? crypto.randomUUID(),
182
+ });
183
+ setState((current) =>
184
+ mergeIdentity(current, workspaceId, response.identity, includeArchived),
185
+ );
186
+ return response;
187
+ }),
188
+ [client, includeArchived, mutate, workspaceId],
189
+ );
190
+
153
191
  const publish = useCallback(
154
192
  async (
155
193
  browserSessionId: string,
@@ -177,6 +215,7 @@ export function useBrowserIdentities(
177
215
  error: visibleState.error,
178
216
  refresh,
179
217
  create,
218
+ update,
180
219
  revisions,
181
220
  publish,
182
221
  };
@@ -209,14 +248,16 @@ function mergeIdentity(
209
248
  current: ReturnType<typeof emptyState>,
210
249
  workspaceId: string,
211
250
  identity: BrowserIdentity,
251
+ includeArchived = false,
212
252
  ): ReturnType<typeof emptyState> {
213
253
  if (current.workspaceId !== workspaceId) return current;
214
254
  return {
215
255
  ...current,
216
- identities: sortIdentities([
217
- ...current.identities.filter((candidate) => candidate.id !== identity.id),
218
- identity,
219
- ]),
256
+ identities: sortIdentities(
257
+ identity.status === "archived" && !includeArchived
258
+ ? current.identities.filter((candidate) => candidate.id !== identity.id)
259
+ : [...current.identities.filter((candidate) => candidate.id !== identity.id), identity],
260
+ ),
220
261
  error: null,
221
262
  };
222
263
  }