@opengeni/react 1.4.2 → 1.4.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.
@@ -867,6 +867,11 @@ function useBrowserSession(options) {
867
867
  const { client, workspaceId } = useEmbeddedBrowserInteraction(options);
868
868
  const browserSessionId = options.browserSessionId;
869
869
  const enabled = (options.enabled ?? true) && browserSessionId !== null;
870
+ const sourceRef = useRef5({ client, workspaceId, browserSessionId, enabled });
871
+ if (sourceRef.current.client !== client || sourceRef.current.workspaceId !== workspaceId || sourceRef.current.browserSessionId !== browserSessionId || sourceRef.current.enabled !== enabled) {
872
+ sourceRef.current = { client, workspaceId, browserSessionId, enabled };
873
+ }
874
+ const source = sourceRef.current;
870
875
  const semanticObservationEnabled = options.semanticObservationEnabled ?? true;
871
876
  const semanticObservationEnabledRef = useRef5(semanticObservationEnabled);
872
877
  semanticObservationEnabledRef.current = semanticObservationEnabled;
@@ -876,13 +881,15 @@ function useBrowserSession(options) {
876
881
  });
877
882
  const pageLive = usePageLiveActivity();
878
883
  const pollIntervalMs = Math.max(750, options.pollIntervalMs ?? 2e3);
879
- const [state, setState] = useState5(() => emptyState4(browserSessionId, enabled));
880
- const visible = state.browserSessionId === browserSessionId ? state : emptyState4(browserSessionId, enabled);
884
+ const [state, setState] = useState5(() => ({ ...emptyState4(browserSessionId, enabled), source }));
885
+ const visible = state.source === source && state.browserSessionId === browserSessionId ? state : { ...emptyState4(browserSessionId, enabled), source };
881
886
  const refreshBlocked = isNonRetryableInteractionError(visible.error);
882
887
  const inputFailureRef = useRef5(visible.inputFailure);
883
888
  inputFailureRef.current = visible.inputFailure;
884
889
  const selectedTargetIdRef = useRef5(visible.selectedTargetId);
885
890
  selectedTargetIdRef.current = visible.selectedTargetId;
891
+ const selectionRevisionRef = useRef5(0);
892
+ const actionResultOrderRef = useRef5({ next: 0, settled: 0 });
886
893
  const selectedTargetRef = useRef5(
887
894
  visible.targets.find((target) => target.id === visible.selectedTargetId) ?? null
888
895
  );
@@ -903,17 +910,23 @@ function useBrowserSession(options) {
903
910
  });
904
911
  const mutationRef = useRef5({
905
912
  browserSessionId,
906
- count: 0
913
+ source,
914
+ count: 0,
915
+ needsInventoryReconcile: false
907
916
  });
908
917
  const mountedRef = useRef5(true);
918
+ const isCurrentSource = useCallback5(
919
+ () => mountedRef.current && sourceRef.current === source,
920
+ [source]
921
+ );
909
922
  const invalidateRefresh = useCallback5(() => {
910
923
  const id = requestRef.current.id + 1;
911
924
  requestRef.current.controller?.abort();
912
925
  requestRef.current = { id, controller: null };
913
926
  }, []);
914
927
  const refreshInventory = useCallback5(
915
- async (checkInput = false) => {
916
- if (!enabled || !browserSessionId) return;
928
+ async (checkInput = false, observeSelectedTarget = true) => {
929
+ if (!enabled || !browserSessionId || !isCurrentSource()) return;
917
930
  const checkedInputFailure = inputFailureRef.current;
918
931
  const id = requestRef.current.id + 1;
919
932
  requestRef.current.controller?.abort();
@@ -928,14 +941,18 @@ function useBrowserSession(options) {
928
941
  signal: controller.signal
929
942
  })
930
943
  ]);
931
- if (!mountedRef.current || requestRef.current.id !== id) return;
944
+ if (!isCurrentSource() || requestRef.current.id !== id) return;
932
945
  const selected = chooseTarget(targetResponse.targets, selectedTargetIdRef.current);
933
946
  selectedTargetIdRef.current = selected?.id ?? null;
947
+ selectedTargetRef.current = selected;
934
948
  const previousObservation = observationRef.current.browserSessionId === browserSessionId ? observationRef.current.observation : null;
935
949
  const retainedObservation = selected && previousObservation && sameObservationTarget(previousObservation, selected) ? previousObservation : null;
936
- observationRef.current = { browserSessionId, observation: retainedObservation };
950
+ observationRef.current = {
951
+ browserSessionId,
952
+ observation: retainedObservation
953
+ };
937
954
  setState(
938
- (current) => current.browserSessionId === browserSessionId ? {
955
+ (current) => isCurrentSource() && current.source === source && current.browserSessionId === browserSessionId ? {
939
956
  ...current,
940
957
  session,
941
958
  targets: targetResponse.targets,
@@ -948,7 +965,7 @@ function useBrowserSession(options) {
948
965
  inputFailure: checkInput && session.id === browserSessionId && targetResponse.browserSessionId === browserSessionId && current.inputFailure === checkedInputFailure ? null : current.inputFailure
949
966
  } : current
950
967
  );
951
- if (!selected || !semanticObservationEnabledRef.current) return;
968
+ if (!selected || !observeSelectedTarget || !semanticObservationEnabledRef.current) return;
952
969
  try {
953
970
  const observation = await client.observeBrowserTarget(
954
971
  workspaceId,
@@ -956,22 +973,29 @@ function useBrowserSession(options) {
956
973
  selected.id,
957
974
  { signal: controller.signal }
958
975
  );
959
- if (!mountedRef.current || requestRef.current.id !== id) return;
976
+ if (!isCurrentSource() || requestRef.current.id !== id) return;
977
+ const currentSelected = selectedTargetRef.current;
978
+ if (observationRef.current.browserSessionId !== browserSessionId || currentSelected?.id !== selected.id || observation.browserSessionId !== browserSessionId || observation.target.id !== selected.id || observation.target.controllerGeneration !== currentSelected.controllerGeneration || observation.target.targetGeneration !== currentSelected.targetGeneration)
979
+ return;
980
+ selectedTargetRef.current = observation.target;
960
981
  observationRef.current = { browserSessionId, observation };
961
982
  lastObservationAtRef.current = Date.now();
962
983
  setState(
963
- (current) => current.browserSessionId === browserSessionId && current.selectedTargetId === observation.target.id ? { ...current, observation } : current
984
+ (current) => isCurrentSource() && current.source === source && current.browserSessionId === browserSessionId && current.selectedTargetId === observation.target.id ? {
985
+ ...current,
986
+ observation,
987
+ targets: replaceTarget(current.targets, observation.target)
988
+ } : current
964
989
  );
965
990
  } catch (cause) {
966
- if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
991
+ if (controller.signal.aborted || !isCurrentSource() || requestRef.current.id !== id)
967
992
  return;
968
993
  void cause;
969
994
  }
970
995
  } catch (cause) {
971
- if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
972
- return;
996
+ if (controller.signal.aborted || !isCurrentSource() || requestRef.current.id !== id) return;
973
997
  setState(
974
- (current) => current.browserSessionId === browserSessionId ? {
998
+ (current) => isCurrentSource() && current.source === source && current.browserSessionId === browserSessionId ? {
975
999
  ...current,
976
1000
  loading: false,
977
1001
  error: cause instanceof Error ? cause : new Error(String(cause))
@@ -983,11 +1007,11 @@ function useBrowserSession(options) {
983
1007
  }
984
1008
  }
985
1009
  },
986
- [browserSessionId, client, enabled, workspaceId]
1010
+ [browserSessionId, client, enabled, isCurrentSource, source, workspaceId]
987
1011
  );
988
1012
  const refresh = useCallback5(async () => await refreshInventory(true), [refreshInventory]);
989
1013
  const refreshSemantic = useCallback5(async () => {
990
- if (!enabled || !browserSessionId) return;
1014
+ if (!enabled || !browserSessionId || !isCurrentSource()) return;
991
1015
  const target = selectedTargetRef.current;
992
1016
  if (!target) return;
993
1017
  const id = requestRef.current.id + 1;
@@ -1001,34 +1025,34 @@ function useBrowserSession(options) {
1001
1025
  target.id,
1002
1026
  { signal: controller.signal }
1003
1027
  );
1004
- if (!mountedRef.current || requestRef.current.id !== id) return;
1028
+ if (!isCurrentSource() || requestRef.current.id !== id) return;
1005
1029
  if (!sameObservationTarget(observation, selectedTargetRef.current)) return;
1006
1030
  observationRef.current = { browserSessionId, observation };
1007
1031
  lastObservationAtRef.current = Date.now();
1008
1032
  setState(
1009
- (current) => current.browserSessionId === browserSessionId && current.selectedTargetId === target.id ? { ...current, observation } : current
1033
+ (current) => isCurrentSource() && current.source === source && current.browserSessionId === browserSessionId && current.selectedTargetId === target.id ? { ...current, observation } : current
1010
1034
  );
1011
1035
  } catch {
1012
1036
  } finally {
1013
1037
  if (requestRef.current.id === id) requestRef.current = { id, controller: null };
1014
1038
  }
1015
- }, [browserSessionId, client, enabled, workspaceId]);
1039
+ }, [browserSessionId, client, enabled, isCurrentSource, source, workspaceId]);
1016
1040
  useEffect5(() => {
1017
1041
  mountedRef.current = true;
1018
1042
  if (!enabled) {
1019
1043
  invalidateRefresh();
1020
- setState(emptyState4(browserSessionId, false));
1044
+ setState({ ...emptyState4(browserSessionId, false), source });
1021
1045
  return;
1022
1046
  }
1023
1047
  setState(
1024
- (current) => current.browserSessionId === browserSessionId ? { ...current, loading: true } : emptyState4(browserSessionId, true)
1048
+ (current) => isCurrentSource() && current.source === source && current.browserSessionId === browserSessionId ? { ...current, loading: true } : { ...emptyState4(browserSessionId, true), source }
1025
1049
  );
1026
1050
  void refreshInventory();
1027
1051
  return () => {
1028
1052
  mountedRef.current = false;
1029
1053
  invalidateRefresh();
1030
1054
  };
1031
- }, [browserSessionId, enabled, invalidateRefresh, refreshInventory]);
1055
+ }, [browserSessionId, enabled, invalidateRefresh, isCurrentSource, refreshInventory, source]);
1032
1056
  useEffect5(() => {
1033
1057
  const previous = previousSemanticObservationRef.current;
1034
1058
  previousSemanticObservationRef.current = {
@@ -1042,11 +1066,19 @@ function useBrowserSession(options) {
1042
1066
  }
1043
1067
  observationRef.current = { browserSessionId, observation: null };
1044
1068
  setState(
1045
- (current) => current.browserSessionId === browserSessionId ? { ...current, observation: null } : current
1069
+ (current) => isCurrentSource() && current.source === source && current.browserSessionId === browserSessionId ? { ...current, observation: null } : current
1046
1070
  );
1047
1071
  void refreshSemantic();
1048
1072
  }
1049
- }, [browserSessionId, enabled, pollIntervalMs, refreshSemantic, semanticObservationEnabled]);
1073
+ }, [
1074
+ browserSessionId,
1075
+ enabled,
1076
+ isCurrentSource,
1077
+ pollIntervalMs,
1078
+ refreshSemantic,
1079
+ semanticObservationEnabled,
1080
+ source
1081
+ ]);
1050
1082
  useEffect5(() => {
1051
1083
  if (!enabled || !pageLive || visible.mutating || refreshBlocked) return;
1052
1084
  const timer = setInterval(() => {
@@ -1058,6 +1090,7 @@ function useBrowserSession(options) {
1058
1090
  if (!enabled || !browserSessionId || !pageLive || refreshBlocked) return;
1059
1091
  let disposed = false;
1060
1092
  const heartbeat = () => {
1093
+ if (!isCurrentSource()) return;
1061
1094
  void client.heartbeatBrowserSession(workspaceId, browserSessionId).catch(() => {
1062
1095
  if (!disposed) void refreshInventory();
1063
1096
  });
@@ -1067,151 +1100,216 @@ function useBrowserSession(options) {
1067
1100
  disposed = true;
1068
1101
  clearInterval(timer);
1069
1102
  };
1070
- }, [browserSessionId, client, enabled, pageLive, refreshInventory, refreshBlocked, workspaceId]);
1103
+ }, [
1104
+ browserSessionId,
1105
+ client,
1106
+ enabled,
1107
+ isCurrentSource,
1108
+ pageLive,
1109
+ refreshInventory,
1110
+ refreshBlocked,
1111
+ workspaceId
1112
+ ]);
1071
1113
  const runMutation = useCallback5(
1072
- async (scopeBrowserSessionId, operation) => {
1114
+ async (scopeBrowserSessionId, operation, selectionRead = false, changesInventory = false) => {
1115
+ if (!enabled || !isCurrentSource()) {
1116
+ throw new Error("The browser source is no longer selected.");
1117
+ }
1118
+ const selectionRevision = ++selectionRevisionRef.current;
1073
1119
  invalidateRefresh();
1074
- if (mutationRef.current.browserSessionId !== scopeBrowserSessionId) {
1120
+ const readRequestId = requestRef.current.id;
1121
+ if (mutationRef.current.browserSessionId !== scopeBrowserSessionId || mutationRef.current.source !== source) {
1075
1122
  mutationRef.current = {
1076
1123
  browserSessionId: scopeBrowserSessionId,
1077
- count: 0
1124
+ source,
1125
+ count: 0,
1126
+ needsInventoryReconcile: false
1078
1127
  };
1079
1128
  }
1080
1129
  mutationRef.current.count += 1;
1081
1130
  setState(
1082
- (current) => current.browserSessionId === scopeBrowserSessionId ? { ...current, mutating: true, error: null } : current
1131
+ (current) => isCurrentSource() && current.source === source && current.browserSessionId === scopeBrowserSessionId ? { ...current, mutating: true, error: null } : current
1083
1132
  );
1084
1133
  try {
1085
- return await operation();
1134
+ return await operation(selectionRevision, readRequestId);
1086
1135
  } catch (cause) {
1087
1136
  const error = cause instanceof Error ? cause : new Error(String(cause));
1088
- setState(
1089
- (current) => current.browserSessionId === scopeBrowserSessionId ? { ...current, error } : current
1090
- );
1137
+ if (!selectionRead || requestRef.current.id === readRequestId) {
1138
+ setState(
1139
+ (current) => isCurrentSource() && selectionRevisionRef.current === selectionRevision && current.source === source && current.browserSessionId === scopeBrowserSessionId ? { ...current, error } : current
1140
+ );
1141
+ }
1091
1142
  throw error;
1092
1143
  } finally {
1093
- if (mutationRef.current.browserSessionId === scopeBrowserSessionId) {
1144
+ if (mutationRef.current.browserSessionId === scopeBrowserSessionId && mutationRef.current.source === source) {
1145
+ if (changesInventory && (selectionRevisionRef.current !== selectionRevision || requestRef.current.id !== readRequestId)) {
1146
+ mutationRef.current.needsInventoryReconcile = true;
1147
+ }
1094
1148
  mutationRef.current.count = Math.max(0, mutationRef.current.count - 1);
1095
1149
  const mutating = mutationRef.current.count > 0;
1096
1150
  setState(
1097
- (current) => current.browserSessionId === scopeBrowserSessionId ? { ...current, mutating } : current
1151
+ (current) => isCurrentSource() && current.source === source && current.browserSessionId === scopeBrowserSessionId ? { ...current, mutating } : current
1098
1152
  );
1153
+ if (!mutating && mutationRef.current.needsInventoryReconcile && isCurrentSource()) {
1154
+ mutationRef.current.needsInventoryReconcile = false;
1155
+ void refreshInventory(false, false);
1156
+ }
1099
1157
  }
1100
1158
  }
1101
1159
  },
1102
- [invalidateRefresh]
1160
+ [enabled, invalidateRefresh, isCurrentSource, refreshInventory, source]
1103
1161
  );
1104
1162
  const selectTarget = useCallback5(
1105
1163
  async (targetId) => {
1106
1164
  if (!browserSessionId) throw new Error("No BrowserSession is selected.");
1107
- return await runMutation(browserSessionId, async () => {
1108
- let targets = visible.targets;
1109
- let target;
1110
- let observation;
1111
- try {
1112
- observation = await client.selectBrowserTarget(workspaceId, browserSessionId, targetId);
1113
- target = observation.target;
1114
- } catch (cause) {
1115
- if (!isMissingBrowserTarget(cause)) throw cause;
1116
- const response = await client.listBrowserTargets(workspaceId, browserSessionId);
1117
- targets = response.targets;
1118
- const fallback = chooseTarget(targets, null);
1119
- if (!fallback) {
1120
- selectedTargetIdRef.current = null;
1121
- observationRef.current = { browserSessionId, observation: null };
1122
- setState(
1123
- (current) => current.browserSessionId === browserSessionId ? {
1124
- ...current,
1125
- targets,
1126
- selectedTargetId: null,
1127
- observation: null
1128
- } : current
1165
+ return await runMutation(
1166
+ browserSessionId,
1167
+ async (selectionRevision, readRequestId) => {
1168
+ const isCurrentSelection = () => isCurrentSource() && selectionRevisionRef.current === selectionRevision;
1169
+ const isCurrentSelectionRead = () => isCurrentSelection() && requestRef.current.id === readRequestId;
1170
+ let targets = visible.targets;
1171
+ let target;
1172
+ let observation;
1173
+ try {
1174
+ observation = await client.selectBrowserTarget(workspaceId, browserSessionId, targetId);
1175
+ target = observation.target;
1176
+ } catch (cause) {
1177
+ if (!isCurrentSelectionRead() || !isMissingBrowserTarget(cause)) throw cause;
1178
+ const response = await client.listBrowserTargets(workspaceId, browserSessionId);
1179
+ if (!isCurrentSelectionRead()) throw cause;
1180
+ targets = response.targets;
1181
+ const fallback = chooseTarget(targets, null);
1182
+ if (!fallback) {
1183
+ selectedTargetIdRef.current = null;
1184
+ selectedTargetRef.current = null;
1185
+ observationRef.current = { browserSessionId, observation: null };
1186
+ setState(
1187
+ (current) => isCurrentSelection() && current.source === source && current.browserSessionId === browserSessionId ? {
1188
+ ...current,
1189
+ targets,
1190
+ selectedTargetId: null,
1191
+ observation: null
1192
+ } : current
1193
+ );
1194
+ throw cause;
1195
+ }
1196
+ observation = await client.selectBrowserTarget(
1197
+ workspaceId,
1198
+ browserSessionId,
1199
+ fallback.id
1129
1200
  );
1130
- throw cause;
1201
+ target = observation.target;
1131
1202
  }
1132
- observation = await client.selectBrowserTarget(
1133
- workspaceId,
1134
- browserSessionId,
1135
- fallback.id
1203
+ if (!isCurrentSelectionRead()) return target;
1204
+ selectedTargetIdRef.current = target.id;
1205
+ selectedTargetRef.current = target;
1206
+ observationRef.current = { browserSessionId, observation };
1207
+ lastObservationAtRef.current = Date.now();
1208
+ setState(
1209
+ (current) => isCurrentSelection() && current.source === source && current.browserSessionId === browserSessionId ? {
1210
+ ...current,
1211
+ targets: replaceTarget(targets, target).map((candidate) => ({
1212
+ ...candidate,
1213
+ selected: candidate.id === target.id
1214
+ })),
1215
+ selectedTargetId: target.id,
1216
+ observation
1217
+ } : current
1136
1218
  );
1137
- target = observation.target;
1138
- }
1139
- selectedTargetIdRef.current = target.id;
1140
- observationRef.current = { browserSessionId, observation };
1141
- lastObservationAtRef.current = Date.now();
1142
- setState(
1143
- (current) => current.browserSessionId === browserSessionId ? {
1144
- ...current,
1145
- targets: targets.map((candidate) => ({
1146
- ...candidate,
1147
- selected: candidate.id === target.id
1148
- })),
1149
- selectedTargetId: target.id,
1150
- observation
1151
- } : current
1152
- );
1153
- return target;
1154
- });
1219
+ return target;
1220
+ },
1221
+ true
1222
+ );
1155
1223
  },
1156
- [browserSessionId, client, runMutation, visible.targets, workspaceId]
1224
+ [browserSessionId, client, isCurrentSource, runMutation, source, visible.targets, workspaceId]
1157
1225
  );
1158
1226
  const openTarget = useCallback5(
1159
1227
  async (url) => {
1160
1228
  if (!browserSessionId) throw new Error("No BrowserSession is selected.");
1161
- return await runMutation(browserSessionId, async () => {
1162
- const observation = await client.openBrowserTarget(
1163
- workspaceId,
1164
- browserSessionId,
1165
- url === void 0 ? {} : { url }
1166
- );
1167
- const target = observation.target;
1168
- selectedTargetIdRef.current = target.id;
1169
- observationRef.current = { browserSessionId, observation };
1170
- lastObservationAtRef.current = Date.now();
1171
- setState(
1172
- (current) => current.browserSessionId === browserSessionId ? {
1173
- ...current,
1174
- targets: [
1175
- ...current.targets.map((candidate) => ({
1176
- ...candidate,
1177
- selected: false
1178
- })),
1179
- target
1180
- ],
1181
- selectedTargetId: target.id,
1182
- observation
1183
- } : current
1184
- );
1185
- return target;
1186
- });
1229
+ return await runMutation(
1230
+ browserSessionId,
1231
+ async (selectionRevision, readRequestId) => {
1232
+ const isCurrentSelection = () => isCurrentSource() && selectionRevisionRef.current === selectionRevision;
1233
+ const isCurrentSelectionRead = () => isCurrentSelection() && requestRef.current.id === readRequestId;
1234
+ const observation = await client.openBrowserTarget(
1235
+ workspaceId,
1236
+ browserSessionId,
1237
+ url === void 0 ? {} : { url }
1238
+ );
1239
+ const target = observation.target;
1240
+ if (!isCurrentSource()) return target;
1241
+ if (!isCurrentSelectionRead()) return target;
1242
+ selectedTargetIdRef.current = target.id;
1243
+ selectedTargetRef.current = target;
1244
+ observationRef.current = { browserSessionId, observation };
1245
+ lastObservationAtRef.current = Date.now();
1246
+ setState(
1247
+ (current) => isCurrentSelection() && current.source === source && current.browserSessionId === browserSessionId ? {
1248
+ ...current,
1249
+ targets: [
1250
+ ...current.targets.map((candidate) => ({
1251
+ ...candidate,
1252
+ selected: false
1253
+ })),
1254
+ target
1255
+ ],
1256
+ selectedTargetId: target.id,
1257
+ observation
1258
+ } : current
1259
+ );
1260
+ return target;
1261
+ },
1262
+ false,
1263
+ true
1264
+ );
1187
1265
  },
1188
- [browserSessionId, client, runMutation, workspaceId]
1266
+ [browserSessionId, client, isCurrentSource, runMutation, source, workspaceId]
1189
1267
  );
1190
1268
  const closeTarget = useCallback5(
1191
1269
  async (targetId) => {
1192
1270
  if (!browserSessionId) throw new Error("No BrowserSession is selected.");
1193
- await runMutation(browserSessionId, async () => {
1194
- const response = await client.closeBrowserTarget(workspaceId, browserSessionId, targetId);
1195
- const selected = chooseTarget(response.targets, null);
1196
- const observation = selected ? await client.observeBrowserTarget(workspaceId, browserSessionId, selected.id) : null;
1197
- selectedTargetIdRef.current = selected?.id ?? null;
1198
- observationRef.current = { browserSessionId, observation };
1199
- if (observation) lastObservationAtRef.current = Date.now();
1200
- setState(
1201
- (current) => current.browserSessionId === browserSessionId ? {
1202
- ...current,
1203
- targets: response.targets,
1204
- selectedTargetId: selected?.id ?? null,
1205
- observation
1206
- } : current
1207
- );
1208
- });
1271
+ await runMutation(
1272
+ browserSessionId,
1273
+ async (selectionRevision, readRequestId) => {
1274
+ const isCurrentSelection = () => isCurrentSource() && selectionRevisionRef.current === selectionRevision;
1275
+ const isCurrentSelectionRead = () => isCurrentSelection() && requestRef.current.id === readRequestId;
1276
+ const response = await client.closeBrowserTarget(workspaceId, browserSessionId, targetId);
1277
+ if (!isCurrentSource()) return;
1278
+ if (!isCurrentSelectionRead()) return;
1279
+ const selected = chooseTarget(response.targets, selectedTargetIdRef.current);
1280
+ const observed = selected ? await client.observeBrowserTarget(workspaceId, browserSessionId, selected.id) : null;
1281
+ if (!isCurrentSource()) return;
1282
+ if (!isCurrentSelectionRead()) return;
1283
+ const observation = observed && selected && observed.browserSessionId === browserSessionId && observed.target.id === selected.id && observed.target.controllerGeneration === selected.controllerGeneration && observed.target.targetGeneration === selected.targetGeneration ? observed : null;
1284
+ const currentTarget = observation?.target ?? selected;
1285
+ selectedTargetIdRef.current = currentTarget?.id ?? null;
1286
+ selectedTargetRef.current = currentTarget;
1287
+ observationRef.current = { browserSessionId, observation };
1288
+ if (observation) lastObservationAtRef.current = Date.now();
1289
+ setState(
1290
+ (current) => isCurrentSelection() && current.source === source && current.browserSessionId === browserSessionId ? {
1291
+ ...current,
1292
+ targets: currentTarget ? replaceTarget(response.targets, currentTarget) : response.targets,
1293
+ selectedTargetId: currentTarget?.id ?? null,
1294
+ observation
1295
+ } : current
1296
+ );
1297
+ },
1298
+ false,
1299
+ true
1300
+ );
1209
1301
  },
1210
- [browserSessionId, client, runMutation, workspaceId]
1302
+ [browserSessionId, client, isCurrentSource, runMutation, source, workspaceId]
1211
1303
  );
1212
1304
  const dispatchAction = useCallback5(
1213
1305
  async (action, operationId, frame, observationMode = "none") => {
1214
1306
  if (!browserSessionId) throw new Error("No BrowserSession is selected.");
1307
+ if (!enabled || !isCurrentSource()) {
1308
+ throw new Error("The browser source is no longer selected.");
1309
+ }
1310
+ if (!selectedTargetRef.current) {
1311
+ throw new Error("The browser page is not ready for input.");
1312
+ }
1215
1313
  if (frame && frame.browserSessionId !== browserSessionId) {
1216
1314
  throw new Error("The displayed browser frame belongs to another BrowserSession.");
1217
1315
  }
@@ -1221,7 +1319,7 @@ function useBrowserSession(options) {
1221
1319
  expectedTargetGeneration: frame.targetGeneration,
1222
1320
  expectedDocumentGeneration: frame.documentGeneration,
1223
1321
  expectedFrameId: frame.frameId
1224
- } : currentObservation ? {
1322
+ } : currentObservation && sameObservationTarget(currentObservation, selectedTargetRef.current) ? {
1225
1323
  targetId: currentObservation.target.id,
1226
1324
  expectedTargetGeneration: currentObservation.target.targetGeneration,
1227
1325
  expectedDocumentGeneration: currentObservation.target.documentGeneration,
@@ -1230,6 +1328,21 @@ function useBrowserSession(options) {
1230
1328
  if (!fence) {
1231
1329
  throw new Error("The browser page is not ready for input.");
1232
1330
  }
1331
+ const expectedControllerGeneration = frame?.controllerGeneration ?? currentObservation.target.controllerGeneration;
1332
+ const selectionRevision = selectionRevisionRef.current;
1333
+ const resultOrder = ++actionResultOrderRef.current.next;
1334
+ let resultDocumentGeneration = fence.expectedDocumentGeneration;
1335
+ const isCurrentResultView = () => {
1336
+ const selected = selectedTargetRef.current;
1337
+ return isCurrentSource() && selectionRevisionRef.current === selectionRevision && selected?.browserSessionId === browserSessionId && selected.id === fence.targetId && selected.controllerGeneration === expectedControllerGeneration && selected.targetGeneration === fence.expectedTargetGeneration && (selected.documentGeneration === fence.expectedDocumentGeneration || selected.documentGeneration === resultDocumentGeneration);
1338
+ };
1339
+ const canProjectResult = () => isCurrentResultView() && resultOrder >= actionResultOrderRef.current.settled;
1340
+ const settleResult = () => {
1341
+ if (!canProjectResult()) return false;
1342
+ actionResultOrderRef.current.settled = resultOrder;
1343
+ invalidateRefresh();
1344
+ return true;
1345
+ };
1233
1346
  try {
1234
1347
  const receipt = await client.actInBrowser(workspaceId, browserSessionId, {
1235
1348
  operationId,
@@ -1237,40 +1350,49 @@ function useBrowserSession(options) {
1237
1350
  observationMode,
1238
1351
  action
1239
1352
  });
1240
- if (receipt.state !== "completed") {
1241
- const inputFailure = {
1242
- operationId: receipt.operationId,
1243
- state: receipt.state,
1244
- error: receipt.error
1245
- };
1246
- setState(
1247
- (current) => current.browserSessionId === browserSessionId ? { ...current, inputFailure } : current
1248
- );
1249
- }
1250
- if (receipt.observation) {
1353
+ resultDocumentGeneration = receipt.observation?.target.documentGeneration ?? fence.expectedDocumentGeneration;
1354
+ const settled = receipt.state === "completed" || receipt.state === "failed" || receipt.state === "outcome_unknown";
1355
+ if (!canProjectResult() || settled && !settleResult()) return receipt;
1356
+ const receivedObservation = receipt.observation;
1357
+ const selectedTarget = selectedTargetRef.current;
1358
+ let projectedObservation = null;
1359
+ if (settled && receivedObservation && canProjectResult() && receipt.browserSessionId === browserSessionId && receipt.controllerGeneration === expectedControllerGeneration && receipt.targetId === fence.targetId && observationRef.current.browserSessionId === browserSessionId && receivedObservation.browserSessionId === browserSessionId && selectedTarget?.browserSessionId === browserSessionId && selectedTarget.id === fence.targetId && receivedObservation.target.id === fence.targetId && selectedTarget.controllerGeneration === expectedControllerGeneration && receivedObservation.target.controllerGeneration === expectedControllerGeneration && selectedTarget.targetGeneration === fence.expectedTargetGeneration && receivedObservation.target.targetGeneration === fence.expectedTargetGeneration && (selectedTarget.documentGeneration === fence.expectedDocumentGeneration || selectedTarget.documentGeneration === receivedObservation.target.documentGeneration)) {
1360
+ projectedObservation = receivedObservation;
1251
1361
  observationRef.current = {
1252
1362
  browserSessionId,
1253
- observation: receipt.observation
1363
+ observation: receivedObservation
1254
1364
  };
1365
+ selectedTargetRef.current = receivedObservation.target;
1255
1366
  lastObservationAtRef.current = Date.now();
1256
- setState(
1257
- (current) => current.browserSessionId === browserSessionId ? {
1258
- ...current,
1259
- observation: receipt.observation,
1260
- targets: replaceTarget(current.targets, receipt.observation.target)
1261
- } : current
1262
- );
1263
1367
  }
1368
+ setState(
1369
+ (current) => isCurrentResultView() && current.source === source && current.browserSessionId === browserSessionId && current.actionResultOrder <= resultOrder ? {
1370
+ ...current,
1371
+ actionResultOrder: settled ? resultOrder : current.actionResultOrder,
1372
+ error: receipt.state === "completed" ? null : current.error,
1373
+ inputFailure: receipt.state === "completed" ? current.inputFailure : {
1374
+ operationId: receipt.operationId,
1375
+ state: receipt.state,
1376
+ error: receipt.error
1377
+ },
1378
+ ...projectedObservation ? {
1379
+ observation: projectedObservation,
1380
+ targets: replaceTarget(current.targets, projectedObservation.target)
1381
+ } : {}
1382
+ } : current
1383
+ );
1264
1384
  return receipt;
1265
1385
  } catch (cause) {
1266
1386
  const error = cause instanceof Error ? cause : new Error(String(cause));
1267
- setState(
1268
- (current) => current.browserSessionId === browserSessionId ? { ...current, error } : current
1269
- );
1387
+ if (settleResult()) {
1388
+ setState(
1389
+ (current) => isCurrentResultView() && current.source === source && current.browserSessionId === browserSessionId && current.actionResultOrder <= resultOrder ? { ...current, error, actionResultOrder: resultOrder } : current
1390
+ );
1391
+ }
1270
1392
  throw error;
1271
1393
  }
1272
1394
  },
1273
- [browserSessionId, client, workspaceId]
1395
+ [browserSessionId, client, enabled, invalidateRefresh, isCurrentSource, source, workspaceId]
1274
1396
  );
1275
1397
  const act = useCallback5(
1276
1398
  async (action, operationId = crypto.randomUUID()) => await dispatchAction(action, operationId, null),
@@ -1282,13 +1404,16 @@ function useBrowserSession(options) {
1282
1404
  );
1283
1405
  const observeForInput = useCallback5(async () => {
1284
1406
  const target = selectedTargetRef.current;
1407
+ if (!enabled || !isCurrentSource()) {
1408
+ throw new Error("The browser source is no longer selected.");
1409
+ }
1285
1410
  if (!browserSessionId || !target) throw new Error("No browser tab is selected.");
1286
1411
  const observation = await client.observeBrowserTarget(workspaceId, browserSessionId, target.id);
1287
- if (!mountedRef.current || observation.browserSessionId !== browserSessionId || !sameObservationTarget(observation, selectedTargetRef.current)) {
1412
+ if (!isCurrentSource() || observation.browserSessionId !== browserSessionId || !sameObservationTarget(observation, selectedTargetRef.current)) {
1288
1413
  throw new Error("The browser page changed. Open the options again.");
1289
1414
  }
1290
1415
  return observation;
1291
- }, [browserSessionId, client, workspaceId]);
1416
+ }, [browserSessionId, client, enabled, isCurrentSource, workspaceId]);
1292
1417
  const actFromObservation = useCallback5(
1293
1418
  async (action, observation) => {
1294
1419
  if (!sameObservationTarget(observation, selectedTargetRef.current)) {
@@ -1296,6 +1421,7 @@ function useBrowserSession(options) {
1296
1421
  }
1297
1422
  return await dispatchAction(action, crypto.randomUUID(), {
1298
1423
  browserSessionId: observation.browserSessionId,
1424
+ controllerGeneration: observation.target.controllerGeneration,
1299
1425
  targetId: observation.target.id,
1300
1426
  targetGeneration: observation.target.targetGeneration,
1301
1427
  documentGeneration: observation.target.documentGeneration,
@@ -1306,6 +1432,9 @@ function useBrowserSession(options) {
1306
1432
  );
1307
1433
  const diagnostics = useCallback5(
1308
1434
  async (diagnosticOptions = {}) => {
1435
+ if (!enabled || !isCurrentSource()) {
1436
+ throw new Error("The browser source is no longer selected.");
1437
+ }
1309
1438
  const targetId = selectedTargetIdRef.current;
1310
1439
  if (!browserSessionId || !targetId) {
1311
1440
  throw new Error("No browser tab is selected.");
@@ -1317,16 +1446,19 @@ function useBrowserSession(options) {
1317
1446
  diagnosticOptions
1318
1447
  );
1319
1448
  },
1320
- [browserSessionId, client, workspaceId]
1449
+ [browserSessionId, client, enabled, isCurrentSource, workspaceId]
1321
1450
  );
1322
1451
  const readClipboard = useCallback5(async () => {
1452
+ if (!enabled || !isCurrentSource()) {
1453
+ throw new Error("The browser source is no longer selected.");
1454
+ }
1323
1455
  if (!browserSessionId) throw new Error("No BrowserSession is selected.");
1324
1456
  const clipboard = await client.readBrowserClipboard(workspaceId, browserSessionId);
1325
- if (clipboard.browserSessionId !== browserSessionId) {
1457
+ if (!isCurrentSource() || clipboard.browserSessionId !== browserSessionId) {
1326
1458
  throw new Error("Browser clipboard belongs to another BrowserSession.");
1327
1459
  }
1328
1460
  return clipboard;
1329
- }, [browserSessionId, client, workspaceId]);
1461
+ }, [browserSessionId, client, enabled, isCurrentSource, workspaceId]);
1330
1462
  return {
1331
1463
  session: visible.session,
1332
1464
  targets: visible.targets,
@@ -1358,7 +1490,8 @@ function emptyState4(browserSessionId, loading) {
1358
1490
  loading,
1359
1491
  mutating: false,
1360
1492
  error: null,
1361
- inputFailure: null
1493
+ inputFailure: null,
1494
+ actionResultOrder: 0
1362
1495
  };
1363
1496
  }
1364
1497
  function chooseTarget(targets, preferredId) {
@@ -4547,4 +4680,4 @@ export {
4547
4680
  useBrowserFrameStream,
4548
4681
  BrowserViewer
4549
4682
  };
4550
- //# sourceMappingURL=chunk-VCSERYXH.js.map
4683
+ //# sourceMappingURL=chunk-2OQA3CPB.js.map