@opengeni/react 1.0.1 → 2.3.0-canary.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.
- package/README.md +33 -10
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
- package/dist/chunk-LS4POUTY.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
- package/dist/chunk-RSXJN73N.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
- package/dist/chunk-SE4W4WQ3.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +6 -3
- package/dist/realtime.js +36 -14
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +50 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +103 -59
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +215 -18
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +53 -17
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +529 -25
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +77 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import type { MachinesResponse } from "../types/machines";
|
|
3
|
+
import { usePageLiveActivity } from "./internal";
|
|
4
|
+
|
|
5
|
+
export const EMPTY_MACHINES: MachinesResponse = {
|
|
6
|
+
activeSandboxId: null,
|
|
7
|
+
activeEpoch: 0,
|
|
8
|
+
machines: [],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SharedMachinesSnapshot = {
|
|
12
|
+
data: MachinesResponse | null;
|
|
13
|
+
loading: boolean;
|
|
14
|
+
error: Error | null;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Subscriber = {
|
|
18
|
+
load: (signal?: AbortSignal) => Promise<MachinesResponse>;
|
|
19
|
+
pollIntervalMs: number | undefined;
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
pageLive: boolean;
|
|
22
|
+
onChange: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type Share = {
|
|
26
|
+
snapshot: SharedMachinesSnapshot;
|
|
27
|
+
subscribers: Set<Subscriber>;
|
|
28
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
29
|
+
disposeTimer: ReturnType<typeof setTimeout> | null;
|
|
30
|
+
abort: AbortController | null;
|
|
31
|
+
inFlight: Promise<void> | null;
|
|
32
|
+
trailing: Promise<void> | null;
|
|
33
|
+
generation: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const shares = new Map<string, Share>();
|
|
37
|
+
|
|
38
|
+
function emit(share: Share): void {
|
|
39
|
+
for (const subscriber of share.subscribers) {
|
|
40
|
+
subscriber.onChange();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function effective(share: Share): { enabled: boolean; pollIntervalMs: number | undefined } {
|
|
45
|
+
let enabled = false;
|
|
46
|
+
let pollIntervalMs: number | undefined;
|
|
47
|
+
for (const subscriber of share.subscribers) {
|
|
48
|
+
if (!subscriber.enabled || !subscriber.pageLive) continue;
|
|
49
|
+
enabled = true;
|
|
50
|
+
if (subscriber.pollIntervalMs !== undefined && subscriber.pollIntervalMs > 0) {
|
|
51
|
+
pollIntervalMs =
|
|
52
|
+
pollIntervalMs === undefined
|
|
53
|
+
? subscriber.pollIntervalMs
|
|
54
|
+
: Math.min(pollIntervalMs, subscriber.pollIntervalMs);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { enabled, pollIntervalMs };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function latestLoad(share: Share): ((signal?: AbortSignal) => Promise<MachinesResponse>) | null {
|
|
61
|
+
let load: ((signal?: AbortSignal) => Promise<MachinesResponse>) | null = null;
|
|
62
|
+
for (const subscriber of share.subscribers) {
|
|
63
|
+
if (subscriber.enabled && subscriber.pageLive) load = subscriber.load;
|
|
64
|
+
}
|
|
65
|
+
return load;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function stopTimer(share: Share): void {
|
|
69
|
+
if (share.timer !== null) {
|
|
70
|
+
clearTimeout(share.timer);
|
|
71
|
+
share.timer = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cancelDispose(share: Share): void {
|
|
76
|
+
if (share.disposeTimer !== null) {
|
|
77
|
+
clearTimeout(share.disposeTimer);
|
|
78
|
+
share.disposeTimer = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function arm(share: Share): void {
|
|
83
|
+
stopTimer(share);
|
|
84
|
+
const { enabled, pollIntervalMs } = effective(share);
|
|
85
|
+
if (!enabled || pollIntervalMs === undefined) return;
|
|
86
|
+
share.timer = setTimeout(() => {
|
|
87
|
+
share.timer = null;
|
|
88
|
+
void run(share).finally(() => arm(share));
|
|
89
|
+
}, pollIntervalMs);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function runFresh(share: Share): Promise<void> {
|
|
93
|
+
const load = latestLoad(share);
|
|
94
|
+
if (!load) return;
|
|
95
|
+
const ticket = ++share.generation;
|
|
96
|
+
const abort = new AbortController();
|
|
97
|
+
share.abort = abort;
|
|
98
|
+
try {
|
|
99
|
+
const data = await load(abort.signal);
|
|
100
|
+
if (ticket !== share.generation || abort.signal.aborted) return;
|
|
101
|
+
share.snapshot = { data, loading: false, error: null };
|
|
102
|
+
emit(share);
|
|
103
|
+
} catch (cause) {
|
|
104
|
+
if (ticket !== share.generation || abort.signal.aborted) return;
|
|
105
|
+
share.snapshot = {
|
|
106
|
+
data: share.snapshot.data,
|
|
107
|
+
loading: false,
|
|
108
|
+
error: cause instanceof Error ? cause : new Error(String(cause)),
|
|
109
|
+
};
|
|
110
|
+
emit(share);
|
|
111
|
+
} finally {
|
|
112
|
+
if (share.abort === abort) share.abort = null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function run(share: Share, force = false): Promise<void> {
|
|
117
|
+
if (!force && share.inFlight) return share.inFlight;
|
|
118
|
+
if (force && share.inFlight) {
|
|
119
|
+
if (share.trailing) return share.trailing;
|
|
120
|
+
let trailing!: Promise<void>;
|
|
121
|
+
trailing = share.inFlight
|
|
122
|
+
.then(async () => {
|
|
123
|
+
if (!effective(share).enabled) return;
|
|
124
|
+
await run(share);
|
|
125
|
+
})
|
|
126
|
+
.finally(() => {
|
|
127
|
+
if (share.trailing === trailing) share.trailing = null;
|
|
128
|
+
});
|
|
129
|
+
share.trailing = trailing;
|
|
130
|
+
return trailing;
|
|
131
|
+
}
|
|
132
|
+
let promise!: Promise<void>;
|
|
133
|
+
promise = runFresh(share).finally(() => {
|
|
134
|
+
if (share.inFlight === promise) share.inFlight = null;
|
|
135
|
+
});
|
|
136
|
+
share.inFlight = promise;
|
|
137
|
+
return promise;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function reconcile(share: Share): void {
|
|
141
|
+
const { enabled } = effective(share);
|
|
142
|
+
if (!enabled) {
|
|
143
|
+
share.generation += 1;
|
|
144
|
+
share.abort?.abort();
|
|
145
|
+
share.abort = null;
|
|
146
|
+
share.inFlight = null;
|
|
147
|
+
stopTimer(share);
|
|
148
|
+
if (share.snapshot.loading) {
|
|
149
|
+
share.snapshot = { ...share.snapshot, loading: false };
|
|
150
|
+
emit(share);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (share.snapshot.data !== null || share.snapshot.error) {
|
|
155
|
+
arm(share);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
share.snapshot = { ...share.snapshot, loading: true };
|
|
159
|
+
emit(share);
|
|
160
|
+
void run(share).finally(() => arm(share));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getOrCreate(shareKey: string): Share {
|
|
164
|
+
const existing = shares.get(shareKey);
|
|
165
|
+
if (existing) {
|
|
166
|
+
cancelDispose(existing);
|
|
167
|
+
return existing;
|
|
168
|
+
}
|
|
169
|
+
const created: Share = {
|
|
170
|
+
snapshot: { data: null, loading: true, error: null },
|
|
171
|
+
subscribers: new Set(),
|
|
172
|
+
timer: null,
|
|
173
|
+
disposeTimer: null,
|
|
174
|
+
abort: null,
|
|
175
|
+
inFlight: null,
|
|
176
|
+
trailing: null,
|
|
177
|
+
generation: 0,
|
|
178
|
+
};
|
|
179
|
+
shares.set(shareKey, created);
|
|
180
|
+
return created;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function refreshSharedMachinesList(shareKey: string): Promise<void> {
|
|
184
|
+
const share = shares.get(shareKey);
|
|
185
|
+
if (!share) return;
|
|
186
|
+
await run(share, true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function useSharedMachinesList(
|
|
190
|
+
shareKey: string,
|
|
191
|
+
load: (signal?: AbortSignal) => Promise<MachinesResponse>,
|
|
192
|
+
options: { pollIntervalMs?: number | undefined; enabled?: boolean | undefined } = {},
|
|
193
|
+
): SharedMachinesSnapshot & { refresh: () => Promise<void> } {
|
|
194
|
+
const enabled = options.enabled ?? true;
|
|
195
|
+
const pageLive = usePageLiveActivity();
|
|
196
|
+
const [, setRevision] = useState(0);
|
|
197
|
+
const live = shares.get(shareKey)?.snapshot;
|
|
198
|
+
const view = live ?? { data: null, loading: enabled && pageLive, error: null };
|
|
199
|
+
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
const share = getOrCreate(shareKey);
|
|
202
|
+
const subscriber: Subscriber = {
|
|
203
|
+
load,
|
|
204
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
205
|
+
enabled,
|
|
206
|
+
pageLive,
|
|
207
|
+
onChange: () => setRevision((revision) => revision + 1),
|
|
208
|
+
};
|
|
209
|
+
share.subscribers.add(subscriber);
|
|
210
|
+
setRevision((revision) => revision + 1);
|
|
211
|
+
reconcile(share);
|
|
212
|
+
return () => {
|
|
213
|
+
share.subscribers.delete(subscriber);
|
|
214
|
+
if (share.subscribers.size > 0) {
|
|
215
|
+
reconcile(share);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Abort immediately so a session switch does not keep the old GET alive.
|
|
219
|
+
// Keep the snapshot one tick so a same-key remount (poll-interval change)
|
|
220
|
+
// does not flash an empty fleet.
|
|
221
|
+
share.generation += 1;
|
|
222
|
+
share.abort?.abort();
|
|
223
|
+
share.abort = null;
|
|
224
|
+
share.inFlight = null;
|
|
225
|
+
share.trailing = null;
|
|
226
|
+
stopTimer(share);
|
|
227
|
+
cancelDispose(share);
|
|
228
|
+
share.disposeTimer = setTimeout(() => {
|
|
229
|
+
share.disposeTimer = null;
|
|
230
|
+
if (share.subscribers.size === 0 && shares.get(shareKey) === share) {
|
|
231
|
+
shares.delete(shareKey);
|
|
232
|
+
}
|
|
233
|
+
}, 0);
|
|
234
|
+
};
|
|
235
|
+
}, [shareKey, load, options.pollIntervalMs, enabled, pageLive]);
|
|
236
|
+
|
|
237
|
+
const refresh = useCallback(() => refreshSharedMachinesList(shareKey), [shareKey]);
|
|
238
|
+
return { ...view, refresh };
|
|
239
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AttachedBrowserDevice } from "@opengeni/sdk/interaction";
|
|
1
|
+
import type { AttachedBrowserBridge, AttachedBrowserDevice } from "@opengeni/sdk/interaction";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import type { EmbeddedBrowserInteractionClientLike } from "../client";
|
|
4
4
|
import {
|
|
@@ -15,6 +15,7 @@ export type UseAttachedBrowsersOptions = EmbeddedBrowserInteractionClientOverrid
|
|
|
15
15
|
|
|
16
16
|
export type UseAttachedBrowsersResult = {
|
|
17
17
|
revision: number;
|
|
18
|
+
bridges: AttachedBrowserBridge[];
|
|
18
19
|
devices: AttachedBrowserDevice[];
|
|
19
20
|
loading: boolean;
|
|
20
21
|
refreshing: boolean;
|
|
@@ -67,6 +68,7 @@ export function useAttachedBrowsers(
|
|
|
67
68
|
setState({
|
|
68
69
|
workspaceId,
|
|
69
70
|
revision: response.revision,
|
|
71
|
+
bridges: response.bridges,
|
|
70
72
|
devices: sortDevices(response.devices),
|
|
71
73
|
loading: false,
|
|
72
74
|
refreshing: false,
|
|
@@ -117,6 +119,7 @@ export function useAttachedBrowsers(
|
|
|
117
119
|
|
|
118
120
|
return {
|
|
119
121
|
revision: visible.revision,
|
|
122
|
+
bridges: visible.bridges,
|
|
120
123
|
devices: visible.devices,
|
|
121
124
|
loading: visible.loading,
|
|
122
125
|
refreshing: visible.refreshing,
|
|
@@ -129,6 +132,7 @@ function emptyState(workspaceId: string, loading: boolean) {
|
|
|
129
132
|
return {
|
|
130
133
|
workspaceId,
|
|
131
134
|
revision: 0,
|
|
135
|
+
bridges: [] as AttachedBrowserBridge[],
|
|
132
136
|
devices: [] as AttachedBrowserDevice[],
|
|
133
137
|
loading,
|
|
134
138
|
refreshing: false,
|
|
@@ -389,7 +389,7 @@ export function useBrowserFrameStream(
|
|
|
389
389
|
connectSocket();
|
|
390
390
|
} catch (cause) {
|
|
391
391
|
if (attachmentAbort.signal.aborted || disposed) return;
|
|
392
|
-
fail(cause);
|
|
392
|
+
fail(cause, !isPlacementGenerationLossError(cause));
|
|
393
393
|
}
|
|
394
394
|
};
|
|
395
395
|
|
|
@@ -423,3 +423,8 @@ async function messageBytes(value: unknown): Promise<Uint8Array> {
|
|
|
423
423
|
}
|
|
424
424
|
throw new Error("browser frame stream returned a non-binary message");
|
|
425
425
|
}
|
|
426
|
+
|
|
427
|
+
function isPlacementGenerationLossError(cause: unknown): boolean {
|
|
428
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
429
|
+
return /placement instance changed|controller authority changed/i.test(message);
|
|
430
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type EmbeddedBrowserInteractionClientOverride,
|
|
17
17
|
useEmbeddedBrowserInteraction,
|
|
18
18
|
} from "../session-context";
|
|
19
|
+
import { isNonRetryableInteractionError } from "../lib/interaction-errors";
|
|
19
20
|
import { usePageLiveActivity } from "./internal";
|
|
20
21
|
|
|
21
22
|
export type UseBrowserSessionOptions = EmbeddedBrowserInteractionClientOverride & {
|
|
@@ -71,6 +72,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
71
72
|
}>(() => emptyState(browserSessionId, enabled));
|
|
72
73
|
const visible =
|
|
73
74
|
state.browserSessionId === browserSessionId ? state : emptyState(browserSessionId, enabled);
|
|
75
|
+
const refreshBlocked = isNonRetryableInteractionError(visible.error);
|
|
74
76
|
const selectedTargetIdRef = useRef<string | null>(visible.selectedTargetId);
|
|
75
77
|
selectedTargetIdRef.current = visible.selectedTargetId;
|
|
76
78
|
const observationRef = useRef<{
|
|
@@ -198,15 +200,15 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
198
200
|
}, [browserSessionId, enabled, invalidateRefresh, refresh]);
|
|
199
201
|
|
|
200
202
|
useEffect(() => {
|
|
201
|
-
if (!enabled || !pageLive || visible.mutating) return;
|
|
203
|
+
if (!enabled || !pageLive || visible.mutating || refreshBlocked) return;
|
|
202
204
|
const timer = setInterval(() => {
|
|
203
205
|
if (!requestRef.current.controller) void refresh();
|
|
204
206
|
}, pollIntervalMs);
|
|
205
207
|
return () => clearInterval(timer);
|
|
206
|
-
}, [enabled, pageLive, pollIntervalMs, refresh, visible.mutating]);
|
|
208
|
+
}, [enabled, pageLive, pollIntervalMs, refresh, refreshBlocked, visible.mutating]);
|
|
207
209
|
|
|
208
210
|
useEffect(() => {
|
|
209
|
-
if (!enabled || !browserSessionId || !pageLive) return;
|
|
211
|
+
if (!enabled || !browserSessionId || !pageLive || refreshBlocked) return;
|
|
210
212
|
let disposed = false;
|
|
211
213
|
const heartbeat = () => {
|
|
212
214
|
void client.heartbeatBrowserSession(workspaceId, browserSessionId).catch(() => {
|
|
@@ -218,7 +220,7 @@ export function useBrowserSession(options: UseBrowserSessionOptions): UseBrowser
|
|
|
218
220
|
disposed = true;
|
|
219
221
|
clearInterval(timer);
|
|
220
222
|
};
|
|
221
|
-
}, [browserSessionId, client, enabled, pageLive, refresh, workspaceId]);
|
|
223
|
+
}, [browserSessionId, client, enabled, pageLive, refresh, refreshBlocked, workspaceId]);
|
|
222
224
|
|
|
223
225
|
const runMutation = useCallback(
|
|
224
226
|
async <T>(scopeBrowserSessionId: string, operation: () => Promise<T>): Promise<T> => {
|
|
@@ -60,6 +60,7 @@ export function useBrowserSessions(
|
|
|
60
60
|
const enabled = options.enabled ?? true;
|
|
61
61
|
const [state, setState] = useState<{
|
|
62
62
|
workspaceId: string;
|
|
63
|
+
active: boolean;
|
|
63
64
|
revision: number;
|
|
64
65
|
sessions: BrowserSession[];
|
|
65
66
|
loading: boolean;
|
|
@@ -89,6 +90,7 @@ export function useBrowserSessions(
|
|
|
89
90
|
requestRef.current = { id, controller };
|
|
90
91
|
setState((current) => ({
|
|
91
92
|
...(current.workspaceId === workspaceId ? current : emptyState(workspaceId, true)),
|
|
93
|
+
active: true,
|
|
92
94
|
loading: foreground,
|
|
93
95
|
refreshing: !foreground,
|
|
94
96
|
error: foreground || current.workspaceId !== workspaceId ? null : current.error,
|
|
@@ -100,6 +102,7 @@ export function useBrowserSessions(
|
|
|
100
102
|
if (!mountedRef.current || requestRef.current.id !== id) return;
|
|
101
103
|
setState({
|
|
102
104
|
workspaceId,
|
|
105
|
+
active: true,
|
|
103
106
|
revision: response.revision,
|
|
104
107
|
sessions: sortBrowserSessions(response.sessions),
|
|
105
108
|
loading: false,
|
|
@@ -229,7 +232,9 @@ export function useBrowserSessions(
|
|
|
229
232
|
revision: visibleState.revision,
|
|
230
233
|
sessions: visibleState.sessions,
|
|
231
234
|
relevantSessions,
|
|
232
|
-
|
|
235
|
+
// A disabled registry is deliberately empty. Expose the first re-enabled
|
|
236
|
+
// render as loading so consumers do not reconcile against that empty gap.
|
|
237
|
+
loading: enabled ? !visibleState.active || visibleState.loading : false,
|
|
233
238
|
refreshing: visibleState.refreshing,
|
|
234
239
|
error: visibleState.error,
|
|
235
240
|
refresh,
|
|
@@ -243,6 +248,7 @@ export function useBrowserSessions(
|
|
|
243
248
|
function emptyState(workspaceId: string, loading: boolean) {
|
|
244
249
|
return {
|
|
245
250
|
workspaceId,
|
|
251
|
+
active: false,
|
|
246
252
|
revision: 0,
|
|
247
253
|
sessions: [] as BrowserSession[],
|
|
248
254
|
loading,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Channel, CreateChannelRequest, Session, UpdateChannelRequest } from "@opengeni/sdk";
|
|
2
|
-
import { useCallback } from "react";
|
|
2
|
+
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
|
3
3
|
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
4
|
+
import { channelCacheFor } from "./channel-cache";
|
|
4
5
|
import { useMutationRunner, usePolledValue } from "./internal";
|
|
5
6
|
|
|
6
7
|
export type UseChannelsOptions = ClientOverride & {
|
|
@@ -15,6 +16,7 @@ export type UseChannelsResult = {
|
|
|
15
16
|
refresh: () => Promise<void>;
|
|
16
17
|
create: (request: CreateChannelRequest) => Promise<Channel | null>;
|
|
17
18
|
update: (channelId: string, request: UpdateChannelRequest) => Promise<Channel | null>;
|
|
19
|
+
reorder: (channelIds: string[]) => Promise<Channel[] | null>;
|
|
18
20
|
remove: (channelId: string) => Promise<boolean>;
|
|
19
21
|
/** Re-file a session into a channel; null moves it back to the inbox. */
|
|
20
22
|
moveSession: (sessionId: string, channelId: string | null) => Promise<Session | null>;
|
|
@@ -30,25 +32,33 @@ export type UseChannelsResult = {
|
|
|
30
32
|
*/
|
|
31
33
|
export function useChannels(options: UseChannelsOptions = {}): UseChannelsResult {
|
|
32
34
|
const { client, workspaceId } = useOpenGeni(options);
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const cache = useMemo(() => channelCacheFor(client, workspaceId), [client, workspaceId]);
|
|
36
|
+
const snapshot = useSyncExternalStore(cache.subscribe, cache.getSnapshot, cache.getSnapshot);
|
|
37
|
+
const load = useCallback(async () => {
|
|
38
|
+
const ticket = cache.beginLoad();
|
|
39
|
+
const channels = await client.listChannels(workspaceId);
|
|
40
|
+
cache.acceptLoad(ticket, channels);
|
|
41
|
+
return channels;
|
|
42
|
+
}, [cache, client, workspaceId]);
|
|
43
|
+
const { loading, error, refresh } = usePolledValue(load, {
|
|
38
44
|
pollIntervalMs: options.pollIntervalMs,
|
|
39
45
|
enabled: options.enabled,
|
|
40
46
|
});
|
|
41
|
-
const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
|
|
47
|
+
const { run, mutating, mutationError, clearMutationError } = useMutationRunner(cache);
|
|
42
48
|
|
|
43
49
|
const create = useCallback(
|
|
44
50
|
async (request: CreateChannelRequest): Promise<Channel | null> => {
|
|
45
51
|
const result = await run(() => client.createChannel(workspaceId, request));
|
|
46
52
|
if (result) {
|
|
53
|
+
// Publish the canonical write response before this promise can resolve.
|
|
54
|
+
// The composer may create a session with this id immediately, while a
|
|
55
|
+
// sibling rail hook could still have an older list request in flight.
|
|
56
|
+
cache.publishCreated(result);
|
|
47
57
|
await refresh();
|
|
48
58
|
}
|
|
49
59
|
return result;
|
|
50
60
|
},
|
|
51
|
-
[client, workspaceId, run, refresh],
|
|
61
|
+
[cache, client, workspaceId, run, refresh],
|
|
52
62
|
);
|
|
53
63
|
|
|
54
64
|
const update = useCallback(
|
|
@@ -76,6 +86,15 @@ export function useChannels(options: UseChannelsOptions = {}): UseChannelsResult
|
|
|
76
86
|
[client, workspaceId, run, refresh],
|
|
77
87
|
);
|
|
78
88
|
|
|
89
|
+
const reorder = useCallback(
|
|
90
|
+
async (channelIds: string[]): Promise<Channel[] | null> => {
|
|
91
|
+
const result = await run(() => client.reorderChannels(workspaceId, { channelIds }));
|
|
92
|
+
if (result) await refresh();
|
|
93
|
+
return result;
|
|
94
|
+
},
|
|
95
|
+
[client, workspaceId, run, refresh],
|
|
96
|
+
);
|
|
97
|
+
|
|
79
98
|
const moveSession = useCallback(
|
|
80
99
|
async (sessionId: string, channelId: string | null): Promise<Session | null> => {
|
|
81
100
|
return await run(() => client.updateSessionChannel(workspaceId, sessionId, { channelId }));
|
|
@@ -84,12 +103,15 @@ export function useChannels(options: UseChannelsOptions = {}): UseChannelsResult
|
|
|
84
103
|
);
|
|
85
104
|
|
|
86
105
|
return {
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
// Cache snapshots replace arrays instead of mutating them, so preserve the
|
|
107
|
+
// stable reference expected from the previous polled-value implementation.
|
|
108
|
+
channels: snapshot.channels as Channel[],
|
|
109
|
+
loading: loading && !snapshot.ready,
|
|
89
110
|
error,
|
|
90
111
|
refresh,
|
|
91
112
|
create,
|
|
92
113
|
update,
|
|
114
|
+
reorder,
|
|
93
115
|
remove,
|
|
94
116
|
moveSession,
|
|
95
117
|
mutating,
|