@opengeni/react 2.5.0-canary.2 → 3.0.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 +15 -0
- package/dist/accounts.d.ts +64 -0
- package/dist/accounts.js +746 -0
- package/dist/accounts.js.map +1 -0
- package/dist/{browser-viewer-GXUICIAG.js → browser-viewer-AEFHUYZK.js} +3 -3
- package/dist/{chunk-MXJGO7LE.js → chunk-D3UIAJKY.js} +52 -2
- package/dist/chunk-D3UIAJKY.js.map +1 -0
- package/dist/{chunk-QGBJ6GHG.js → chunk-ELIYWBZR.js} +4 -4
- package/dist/{chunk-6H6S5V4Z.js → chunk-HQVCKCLG.js} +758 -871
- package/dist/chunk-HQVCKCLG.js.map +1 -0
- package/dist/{chunk-T7VAYG2I.js → chunk-MZ2G2XBA.js} +54 -2
- package/dist/chunk-MZ2G2XBA.js.map +1 -0
- package/dist/{chunk-X4JAFD5R.js → chunk-OAM4WAAM.js} +4 -4
- package/dist/{chunk-ZUT5QSQB.js → chunk-RGALCTWO.js} +57 -17
- package/dist/{chunk-ZUT5QSQB.js.map → chunk-RGALCTWO.js.map} +1 -1
- package/dist/chunk-YFKJ7VZH.js +268 -0
- package/dist/chunk-YFKJ7VZH.js.map +1 -0
- package/dist/{chunk-ATQJEWCA.js → chunk-YJFGAFBD.js} +124 -50
- package/dist/chunk-YJFGAFBD.js.map +1 -0
- package/dist/components/composer.d.ts +8 -0
- package/dist/components/file-browser.d.ts +4 -1
- package/dist/components/sandbox-files.d.ts +4 -1
- package/dist/components/sandbox-workspace.d.ts +5 -0
- package/dist/composer.js +3 -3
- package/dist/{computer-viewer-ZOP4Q7A7.js → computer-viewer-FWEZISW4.js} +3 -3
- package/dist/embedded-session-client.d.ts +23 -0
- package/dist/file-node-visibility.d.ts +15 -0
- package/dist/hooks/use-file-attachments.d.ts +9 -2
- package/dist/human-input.d.ts +3 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +139 -53
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +3 -3
- package/dist/session-ui.js +4 -4
- package/dist/session.d.ts +2 -0
- package/dist/session.js +7 -5
- package/dist/timeline/index.d.ts +1 -0
- package/dist/timeline/screenshot-lightbox.d.ts +16 -1
- package/package.json +6 -2
- package/src/accounts.tsx +970 -0
- package/src/components/chat-composer.tsx +88 -84
- package/src/components/composer.tsx +53 -11
- package/src/components/file-browser.tsx +33 -20
- package/src/components/human-input-surface.tsx +10 -6
- package/src/components/message-timeline.tsx +37 -9
- package/src/components/sandbox-files.tsx +42 -2
- package/src/components/sandbox-workspace.tsx +10 -0
- package/src/embedded-session-client.ts +104 -0
- package/src/file-node-visibility.ts +66 -0
- package/src/hooks/use-composer.ts +64 -2
- package/src/hooks/use-file-attachments.ts +38 -6
- package/src/hooks/use-human-input.ts +31 -4
- package/src/human-input.ts +12 -1
- package/src/index.ts +12 -0
- package/src/session.ts +5 -0
- package/src/timeline/index.ts +1 -0
- package/src/timeline/screenshot-lightbox.tsx +93 -29
- package/styles/compiled.css +11 -1
- package/styles/tokens.css +3 -1
- package/dist/chunk-6H6S5V4Z.js.map +0 -1
- package/dist/chunk-ATQJEWCA.js.map +0 -1
- package/dist/chunk-FK6VG4LL.js +0 -90
- package/dist/chunk-FK6VG4LL.js.map +0 -1
- package/dist/chunk-MXJGO7LE.js.map +0 -1
- package/dist/chunk-T7VAYG2I.js.map +0 -1
- /package/dist/{browser-viewer-GXUICIAG.js.map → browser-viewer-AEFHUYZK.js.map} +0 -0
- /package/dist/{chunk-QGBJ6GHG.js.map → chunk-ELIYWBZR.js.map} +0 -0
- /package/dist/{chunk-X4JAFD5R.js.map → chunk-OAM4WAAM.js.map} +0 -0
- /package/dist/{computer-viewer-ZOP4Q7A7.js.map → computer-viewer-FWEZISW4.js.map} +0 -0
package/dist/accounts.js
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
// src/accounts.tsx
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState
|
|
10
|
+
} from "react";
|
|
11
|
+
import { jsx } from "react/jsx-runtime";
|
|
12
|
+
var BrowserAccountsContext = createContext(null);
|
|
13
|
+
var DEFAULT_CHANNEL = "opengeni:browser-account-epoch:v1";
|
|
14
|
+
var CROSS_TAB_ACTOR_HOLD_MS = 3e4;
|
|
15
|
+
async function yieldToCrossTabActorHold() {
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
17
|
+
}
|
|
18
|
+
function operationId() {
|
|
19
|
+
return crypto.randomUUID();
|
|
20
|
+
}
|
|
21
|
+
function sameActor(left, right) {
|
|
22
|
+
return left?.generation === right?.generation && left?.actorEpoch === right?.actorEpoch && left?.selectedSlotId === right?.selectedSlotId && left?.state === right?.state;
|
|
23
|
+
}
|
|
24
|
+
function sameSelectedActor(left, right) {
|
|
25
|
+
return left !== null && right !== null && left.actorEpoch === right.actorEpoch && left.selectedSlotId === right.selectedSlotId && left.state === right.state;
|
|
26
|
+
}
|
|
27
|
+
function compareCounter(left, right) {
|
|
28
|
+
const leftValue = BigInt(left);
|
|
29
|
+
const rightValue = BigInt(right);
|
|
30
|
+
return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
|
|
31
|
+
}
|
|
32
|
+
function isOlderProjection(candidate, current) {
|
|
33
|
+
const epoch = compareCounter(candidate.actorEpoch, current.actorEpoch);
|
|
34
|
+
return epoch < 0 || epoch === 0 && compareCounter(candidate.generation, current.generation) < 0;
|
|
35
|
+
}
|
|
36
|
+
function accountError(error) {
|
|
37
|
+
return error instanceof Error ? error : new Error("Browser account operation failed");
|
|
38
|
+
}
|
|
39
|
+
function supersededOperationError() {
|
|
40
|
+
const error = new Error("Browser account operation was superseded by an actor change");
|
|
41
|
+
error.name = "AbortError";
|
|
42
|
+
return error;
|
|
43
|
+
}
|
|
44
|
+
function errorCode(error) {
|
|
45
|
+
if (!error || typeof error !== "object") return null;
|
|
46
|
+
const code = error.code;
|
|
47
|
+
return typeof code === "string" ? code : null;
|
|
48
|
+
}
|
|
49
|
+
function needsReconciliation(error) {
|
|
50
|
+
return (/* @__PURE__ */ new Set(["actor_change_required", "generation_conflict", "operation_outcome_unknown"])).has(
|
|
51
|
+
errorCode(error) ?? ""
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
function BrowserAccountsProvider({
|
|
55
|
+
client,
|
|
56
|
+
onActorTransition,
|
|
57
|
+
children,
|
|
58
|
+
broadcastChannelName = DEFAULT_CHANNEL,
|
|
59
|
+
bootstrapLegacySession = false
|
|
60
|
+
}) {
|
|
61
|
+
const [projection, setProjection] = useState(null);
|
|
62
|
+
const [phase, setPhase] = useState("loading");
|
|
63
|
+
const [blockers, setBlockers] = useState([]);
|
|
64
|
+
const [error, setError] = useState(null);
|
|
65
|
+
const [transaction, setTransaction] = useState(null);
|
|
66
|
+
const projectionRef = useRef(projection);
|
|
67
|
+
const transactionRef = useRef(transaction);
|
|
68
|
+
const blockerInspectorsRef = useRef(/* @__PURE__ */ new Map());
|
|
69
|
+
const pendingRef = useRef(null);
|
|
70
|
+
const transactionTargetSlotIdRef = useRef(null);
|
|
71
|
+
const beginOperationRef = useRef(null);
|
|
72
|
+
const bootstrapOperationRef = useRef(null);
|
|
73
|
+
const completionOperationRef = useRef(null);
|
|
74
|
+
const cancelOperationRef = useRef(null);
|
|
75
|
+
const pendingCommitSequenceRef = useRef(null);
|
|
76
|
+
const invalidatedDuringPendingCommitRef = useRef(false);
|
|
77
|
+
const sequenceRef = useRef(0);
|
|
78
|
+
const transitionAbortRef = useRef(null);
|
|
79
|
+
const channelRef = useRef(null);
|
|
80
|
+
const crossTabActorHoldsRef = useRef(/* @__PURE__ */ new Map());
|
|
81
|
+
projectionRef.current = projection;
|
|
82
|
+
transactionRef.current = transaction;
|
|
83
|
+
const inspectBlockers = useCallback(() => {
|
|
84
|
+
const current = [...blockerInspectorsRef.current.entries()].map(([id, inspect]) => {
|
|
85
|
+
const blocker = inspect();
|
|
86
|
+
return blocker ? { ...blocker, id } : null;
|
|
87
|
+
}).filter((value2) => value2 !== null).sort((left, right) => left.id.localeCompare(right.id));
|
|
88
|
+
setBlockers(current);
|
|
89
|
+
return current;
|
|
90
|
+
}, []);
|
|
91
|
+
const publishActorHint = useCallback((next) => {
|
|
92
|
+
if (!next) return;
|
|
93
|
+
channelRef.current?.postMessage({
|
|
94
|
+
type: "actor-epoch-changed",
|
|
95
|
+
generation: next.generation,
|
|
96
|
+
actorEpoch: next.actorEpoch
|
|
97
|
+
});
|
|
98
|
+
}, []);
|
|
99
|
+
const publishActorHold = useCallback(
|
|
100
|
+
(transitionId, current) => {
|
|
101
|
+
channelRef.current?.postMessage({
|
|
102
|
+
type: "actor-transition-pending",
|
|
103
|
+
transitionId,
|
|
104
|
+
generation: current.generation,
|
|
105
|
+
actorEpoch: current.actorEpoch
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
[]
|
|
109
|
+
);
|
|
110
|
+
const publishActorRelease = useCallback((transitionId) => {
|
|
111
|
+
channelRef.current?.postMessage({
|
|
112
|
+
type: "actor-transition-released",
|
|
113
|
+
transitionId
|
|
114
|
+
});
|
|
115
|
+
}, []);
|
|
116
|
+
const clearLoginTransactionState = useCallback(() => {
|
|
117
|
+
transactionRef.current = null;
|
|
118
|
+
beginOperationRef.current = null;
|
|
119
|
+
completionOperationRef.current = null;
|
|
120
|
+
cancelOperationRef.current = null;
|
|
121
|
+
transactionTargetSlotIdRef.current = null;
|
|
122
|
+
setTransaction(null);
|
|
123
|
+
}, []);
|
|
124
|
+
const settleActorTransition = useCallback(
|
|
125
|
+
async (kind, from, accepted, sequence, publish, authorityResetConfirmed = false) => {
|
|
126
|
+
let source = from;
|
|
127
|
+
let target = accepted;
|
|
128
|
+
if (!authorityResetConfirmed && from && target && isOlderProjection(target, from)) {
|
|
129
|
+
target = await client.getSessionSet();
|
|
130
|
+
if (sequenceRef.current !== sequence) return null;
|
|
131
|
+
if (isOlderProjection(target, from)) {
|
|
132
|
+
throw new Error("Browser account response regressed the accepted actor epoch");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (sameSelectedActor(source, target)) {
|
|
136
|
+
setProjection(target);
|
|
137
|
+
setPhase("ready");
|
|
138
|
+
setError(null);
|
|
139
|
+
return target;
|
|
140
|
+
}
|
|
141
|
+
while (true) {
|
|
142
|
+
transitionAbortRef.current?.abort();
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
transitionAbortRef.current = controller;
|
|
145
|
+
if (publish) publishActorHint(target);
|
|
146
|
+
setPhase("loading");
|
|
147
|
+
await onActorTransition({ kind, from: source, to: target, signal: controller.signal });
|
|
148
|
+
if (controller.signal.aborted || sequenceRef.current !== sequence) return null;
|
|
149
|
+
if (!target) {
|
|
150
|
+
setProjection(null);
|
|
151
|
+
setPhase("ready");
|
|
152
|
+
setError(null);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const confirmed = await client.reconcileSessionSetAuthority();
|
|
156
|
+
if (controller.signal.aborted || sequenceRef.current !== sequence) return null;
|
|
157
|
+
if (sameActor(target, confirmed) || sameSelectedActor(target, confirmed)) {
|
|
158
|
+
setProjection(confirmed);
|
|
159
|
+
setPhase("ready");
|
|
160
|
+
setError(null);
|
|
161
|
+
return confirmed;
|
|
162
|
+
}
|
|
163
|
+
source = target;
|
|
164
|
+
target = confirmed;
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
[client, onActorTransition, publishActorHint]
|
|
168
|
+
);
|
|
169
|
+
const reconcile = useCallback(
|
|
170
|
+
async (kind, publish = false) => {
|
|
171
|
+
const sequence = ++sequenceRef.current;
|
|
172
|
+
const before = projectionRef.current;
|
|
173
|
+
setPhase("loading");
|
|
174
|
+
let first = await client.reconcileSessionSetAuthority();
|
|
175
|
+
if (sequenceRef.current !== sequence) return null;
|
|
176
|
+
if (bootstrapLegacySession && first.mode === "dual" && first.selectedSlotId === null && first.slots.length === 0) {
|
|
177
|
+
const bootstrapOperation = bootstrapOperationRef.current?.generation === first.generation ? bootstrapOperationRef.current.operationId : operationId();
|
|
178
|
+
bootstrapOperationRef.current = {
|
|
179
|
+
generation: first.generation,
|
|
180
|
+
operationId: bootstrapOperation
|
|
181
|
+
};
|
|
182
|
+
try {
|
|
183
|
+
first = await client.bootstrapSessionSet({
|
|
184
|
+
operationId: bootstrapOperation,
|
|
185
|
+
expectedGeneration: first.generation
|
|
186
|
+
});
|
|
187
|
+
bootstrapOperationRef.current = null;
|
|
188
|
+
} catch (caught) {
|
|
189
|
+
if (!needsReconciliation(caught)) throw caught;
|
|
190
|
+
const reconciled = await client.getSessionSet();
|
|
191
|
+
if (sequenceRef.current !== sequence) return null;
|
|
192
|
+
if (reconciled.selectedSlotId === null || reconciled.slots.length === 0) throw caught;
|
|
193
|
+
first = reconciled;
|
|
194
|
+
bootstrapOperationRef.current = null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (sameActor(before, first) || sameSelectedActor(before, first)) {
|
|
198
|
+
setProjection(first);
|
|
199
|
+
setPhase("ready");
|
|
200
|
+
setError(null);
|
|
201
|
+
return first;
|
|
202
|
+
}
|
|
203
|
+
if (kind === "cross_tab") clearLoginTransactionState();
|
|
204
|
+
return await settleActorTransition(kind, before, first, sequence, publish, true);
|
|
205
|
+
},
|
|
206
|
+
[bootstrapLegacySession, clearLoginTransactionState, client, settleActorTransition]
|
|
207
|
+
);
|
|
208
|
+
const fail = useCallback(
|
|
209
|
+
async (caught, kind) => {
|
|
210
|
+
const nextError = accountError(caught);
|
|
211
|
+
if (needsReconciliation(caught)) {
|
|
212
|
+
try {
|
|
213
|
+
await reconcile(kind);
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
setError(nextError);
|
|
218
|
+
setPhase("recoverable_error");
|
|
219
|
+
throw nextError;
|
|
220
|
+
},
|
|
221
|
+
[reconcile]
|
|
222
|
+
);
|
|
223
|
+
const executePending = useCallback(
|
|
224
|
+
async (pending) => {
|
|
225
|
+
const blocked = inspectBlockers();
|
|
226
|
+
if (blocked.length > 0) {
|
|
227
|
+
pendingRef.current = pending;
|
|
228
|
+
setPhase("blocked");
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
const current = projectionRef.current;
|
|
232
|
+
if (!current) throw new Error("Browser session set is not loaded");
|
|
233
|
+
const expectedGeneration = pending.expectedGeneration ?? current.generation;
|
|
234
|
+
pending.expectedGeneration = expectedGeneration;
|
|
235
|
+
const sequence = ++sequenceRef.current;
|
|
236
|
+
pendingCommitSequenceRef.current = sequence;
|
|
237
|
+
invalidatedDuringPendingCommitRef.current = false;
|
|
238
|
+
pendingRef.current = pending;
|
|
239
|
+
setPhase("committing");
|
|
240
|
+
setError(null);
|
|
241
|
+
const changesActor = pending.changesActor(current);
|
|
242
|
+
if (changesActor) {
|
|
243
|
+
publishActorHold(pending.operationId, current);
|
|
244
|
+
await yieldToCrossTabActorHold();
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
let accepted;
|
|
248
|
+
try {
|
|
249
|
+
accepted = await pending.execute(expectedGeneration);
|
|
250
|
+
} catch (caught) {
|
|
251
|
+
if (errorCode(caught) !== "operation_outcome_unknown") throw caught;
|
|
252
|
+
accepted = await pending.execute(expectedGeneration);
|
|
253
|
+
}
|
|
254
|
+
if (sequenceRef.current !== sequence) return false;
|
|
255
|
+
let authorityResetConfirmed = pending.kind === "logout_all";
|
|
256
|
+
if (invalidatedDuringPendingCommitRef.current) {
|
|
257
|
+
accepted = await client.reconcileSessionSetAuthority();
|
|
258
|
+
authorityResetConfirmed = true;
|
|
259
|
+
if (sequenceRef.current !== sequence) return false;
|
|
260
|
+
}
|
|
261
|
+
pendingRef.current = null;
|
|
262
|
+
if (!authorityResetConfirmed && sameSelectedActor(current, accepted)) {
|
|
263
|
+
setProjection(accepted);
|
|
264
|
+
setPhase("ready");
|
|
265
|
+
setError(null);
|
|
266
|
+
} else {
|
|
267
|
+
await settleActorTransition(
|
|
268
|
+
pending.kind,
|
|
269
|
+
current,
|
|
270
|
+
accepted,
|
|
271
|
+
sequence,
|
|
272
|
+
true,
|
|
273
|
+
authorityResetConfirmed
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
} catch (caught) {
|
|
278
|
+
return await fail(caught, pending.kind);
|
|
279
|
+
} finally {
|
|
280
|
+
if (changesActor) publishActorRelease(pending.operationId);
|
|
281
|
+
if (pendingCommitSequenceRef.current === sequence) {
|
|
282
|
+
pendingCommitSequenceRef.current = null;
|
|
283
|
+
invalidatedDuringPendingCommitRef.current = false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
[client, fail, inspectBlockers, publishActorHold, publishActorRelease, settleActorTransition]
|
|
288
|
+
);
|
|
289
|
+
const refresh = useCallback(async () => {
|
|
290
|
+
try {
|
|
291
|
+
return await reconcile("cross_tab");
|
|
292
|
+
} catch (caught) {
|
|
293
|
+
const nextError = accountError(caught);
|
|
294
|
+
setError(nextError);
|
|
295
|
+
setPhase("recoverable_error");
|
|
296
|
+
throw nextError;
|
|
297
|
+
}
|
|
298
|
+
}, [reconcile]);
|
|
299
|
+
const beginNeutralActorInvalidation = useCallback(
|
|
300
|
+
async (before) => {
|
|
301
|
+
transitionAbortRef.current?.abort();
|
|
302
|
+
const controller = new AbortController();
|
|
303
|
+
transitionAbortRef.current = controller;
|
|
304
|
+
projectionRef.current = null;
|
|
305
|
+
setProjection(null);
|
|
306
|
+
clearLoginTransactionState();
|
|
307
|
+
setPhase("loading");
|
|
308
|
+
setError(null);
|
|
309
|
+
await onActorTransition({
|
|
310
|
+
kind: "cross_tab",
|
|
311
|
+
from: before,
|
|
312
|
+
to: null,
|
|
313
|
+
signal: controller.signal
|
|
314
|
+
});
|
|
315
|
+
return controller;
|
|
316
|
+
},
|
|
317
|
+
[clearLoginTransactionState, onActorTransition]
|
|
318
|
+
);
|
|
319
|
+
const invalidateActor = useCallback(async () => {
|
|
320
|
+
if (pendingCommitSequenceRef.current !== null) {
|
|
321
|
+
invalidatedDuringPendingCommitRef.current = true;
|
|
322
|
+
await beginNeutralActorInvalidation(projectionRef.current);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const sequence = ++sequenceRef.current;
|
|
326
|
+
const before = projectionRef.current;
|
|
327
|
+
try {
|
|
328
|
+
const neutral = await beginNeutralActorInvalidation(before);
|
|
329
|
+
if (neutral.signal.aborted || sequenceRef.current !== sequence) return null;
|
|
330
|
+
const accepted = await client.reconcileSessionSetAuthority();
|
|
331
|
+
if (sequenceRef.current !== sequence) return null;
|
|
332
|
+
return await settleActorTransition("cross_tab", before, accepted, sequence, false, true);
|
|
333
|
+
} catch (caught) {
|
|
334
|
+
if (sequenceRef.current !== sequence) return null;
|
|
335
|
+
const nextError = accountError(caught);
|
|
336
|
+
setError(nextError);
|
|
337
|
+
setPhase("recoverable_error");
|
|
338
|
+
throw nextError;
|
|
339
|
+
}
|
|
340
|
+
}, [beginNeutralActorInvalidation, client, settleActorTransition]);
|
|
341
|
+
const registerTransitionBlocker = useCallback((id, inspect) => {
|
|
342
|
+
if (!id.trim()) throw new Error("Browser account transition blocker id is required");
|
|
343
|
+
blockerInspectorsRef.current.set(id, inspect);
|
|
344
|
+
return () => {
|
|
345
|
+
if (blockerInspectorsRef.current.get(id) === inspect) {
|
|
346
|
+
blockerInspectorsRef.current.delete(id);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}, []);
|
|
350
|
+
const continueTransition = useCallback(async () => {
|
|
351
|
+
const pending = pendingRef.current;
|
|
352
|
+
return pending ? await executePending(pending) : false;
|
|
353
|
+
}, [executePending]);
|
|
354
|
+
const cancelPendingTransition = useCallback(() => {
|
|
355
|
+
pendingRef.current = null;
|
|
356
|
+
setBlockers([]);
|
|
357
|
+
setPhase(projectionRef.current ? "ready" : "loading");
|
|
358
|
+
}, []);
|
|
359
|
+
const beginLogin = useCallback(
|
|
360
|
+
async (kind, slotId, returnIntent) => {
|
|
361
|
+
const current = projectionRef.current;
|
|
362
|
+
if (!current) throw new Error("Browser session set is not loaded");
|
|
363
|
+
if (kind === "reauth" && slotId === current.selectedSlotId && inspectBlockers().length > 0) {
|
|
364
|
+
setPhase("blocked");
|
|
365
|
+
throw new Error("Settle account transition blockers before re-authenticating");
|
|
366
|
+
}
|
|
367
|
+
const sequence = ++sequenceRef.current;
|
|
368
|
+
setPhase("committing");
|
|
369
|
+
setError(null);
|
|
370
|
+
const signature = JSON.stringify([
|
|
371
|
+
kind,
|
|
372
|
+
slotId ?? null,
|
|
373
|
+
returnIntent ?? null,
|
|
374
|
+
current.generation
|
|
375
|
+
]);
|
|
376
|
+
const beginOperation = beginOperationRef.current?.signature === signature ? beginOperationRef.current.operationId : operationId();
|
|
377
|
+
beginOperationRef.current = { signature, operationId: beginOperation };
|
|
378
|
+
let next;
|
|
379
|
+
try {
|
|
380
|
+
next = await client.beginLoginTransaction({
|
|
381
|
+
operationId: beginOperation,
|
|
382
|
+
expectedGeneration: current.generation,
|
|
383
|
+
kind,
|
|
384
|
+
...slotId ? { slotId } : {},
|
|
385
|
+
...returnIntent ? { returnIntent } : {}
|
|
386
|
+
});
|
|
387
|
+
} catch (caught) {
|
|
388
|
+
if (sequenceRef.current !== sequence) {
|
|
389
|
+
if (beginOperationRef.current?.operationId === beginOperation) {
|
|
390
|
+
beginOperationRef.current = null;
|
|
391
|
+
}
|
|
392
|
+
throw supersededOperationError();
|
|
393
|
+
}
|
|
394
|
+
return await fail(caught, kind);
|
|
395
|
+
}
|
|
396
|
+
if (sequenceRef.current !== sequence) {
|
|
397
|
+
if (beginOperationRef.current?.operationId === beginOperation) {
|
|
398
|
+
beginOperationRef.current = null;
|
|
399
|
+
}
|
|
400
|
+
throw supersededOperationError();
|
|
401
|
+
}
|
|
402
|
+
beginOperationRef.current = null;
|
|
403
|
+
transactionTargetSlotIdRef.current = slotId ?? null;
|
|
404
|
+
completionOperationRef.current = null;
|
|
405
|
+
cancelOperationRef.current = null;
|
|
406
|
+
setTransaction(next);
|
|
407
|
+
setPhase("ready");
|
|
408
|
+
return next;
|
|
409
|
+
},
|
|
410
|
+
[client, fail, inspectBlockers]
|
|
411
|
+
);
|
|
412
|
+
const beginAdd = useCallback(
|
|
413
|
+
async (returnIntent) => await beginLogin("add", void 0, returnIntent),
|
|
414
|
+
[beginLogin]
|
|
415
|
+
);
|
|
416
|
+
const beginReauth = useCallback(
|
|
417
|
+
async (slotId, returnIntent) => await beginLogin("reauth", slotId, returnIntent),
|
|
418
|
+
[beginLogin]
|
|
419
|
+
);
|
|
420
|
+
const completeEmailPassword = useCallback(
|
|
421
|
+
async (input) => {
|
|
422
|
+
const current = projectionRef.current;
|
|
423
|
+
const activeTransaction = transactionRef.current;
|
|
424
|
+
if (!current || !activeTransaction) throw new Error("No browser login transaction is active");
|
|
425
|
+
const selectedReauth = activeTransaction.kind === "reauth" && transactionTargetSlotIdRef.current !== null && transactionTargetSlotIdRef.current === current.selectedSlotId;
|
|
426
|
+
if (selectedReauth && inspectBlockers().length > 0) {
|
|
427
|
+
setPhase("blocked");
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
const sequence = ++sequenceRef.current;
|
|
431
|
+
setPhase("committing");
|
|
432
|
+
setError(null);
|
|
433
|
+
const completionOperation = completionOperationRef.current ?? {
|
|
434
|
+
operationId: operationId(),
|
|
435
|
+
expectedGeneration: current.generation
|
|
436
|
+
};
|
|
437
|
+
completionOperationRef.current = completionOperation;
|
|
438
|
+
try {
|
|
439
|
+
const completed = await client.completeEmailPasswordTransaction({
|
|
440
|
+
operationId: completionOperation.operationId,
|
|
441
|
+
expectedGeneration: completionOperation.expectedGeneration,
|
|
442
|
+
transactionId: activeTransaction.id,
|
|
443
|
+
email: input.email,
|
|
444
|
+
password: input.password
|
|
445
|
+
});
|
|
446
|
+
if (sequenceRef.current !== sequence) return false;
|
|
447
|
+
completionOperationRef.current = null;
|
|
448
|
+
cancelOperationRef.current = null;
|
|
449
|
+
transactionTargetSlotIdRef.current = null;
|
|
450
|
+
setTransaction(null);
|
|
451
|
+
const actorChanged = completed.projection.actorEpoch !== current.actorEpoch;
|
|
452
|
+
if (actorChanged) {
|
|
453
|
+
await settleActorTransition(
|
|
454
|
+
activeTransaction.kind,
|
|
455
|
+
current,
|
|
456
|
+
completed.projection,
|
|
457
|
+
sequence,
|
|
458
|
+
true
|
|
459
|
+
);
|
|
460
|
+
} else {
|
|
461
|
+
setProjection(completed.projection);
|
|
462
|
+
setPhase("ready");
|
|
463
|
+
}
|
|
464
|
+
return true;
|
|
465
|
+
} catch (caught) {
|
|
466
|
+
return await fail(caught, activeTransaction.kind);
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
[client, fail, inspectBlockers, settleActorTransition]
|
|
470
|
+
);
|
|
471
|
+
const settleExternalLoginTransaction = useCallback(
|
|
472
|
+
async (transactionId) => {
|
|
473
|
+
const activeTransaction = transactionRef.current;
|
|
474
|
+
if (!activeTransaction || activeTransaction.id !== transactionId) return false;
|
|
475
|
+
try {
|
|
476
|
+
await reconcile(activeTransaction.kind, true);
|
|
477
|
+
if (transactionRef.current?.id !== transactionId) return false;
|
|
478
|
+
completionOperationRef.current = null;
|
|
479
|
+
cancelOperationRef.current = null;
|
|
480
|
+
transactionTargetSlotIdRef.current = null;
|
|
481
|
+
setTransaction(null);
|
|
482
|
+
return true;
|
|
483
|
+
} catch (caught) {
|
|
484
|
+
const nextError = accountError(caught);
|
|
485
|
+
setError(nextError);
|
|
486
|
+
setPhase("recoverable_error");
|
|
487
|
+
throw nextError;
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
[reconcile]
|
|
491
|
+
);
|
|
492
|
+
const cancelLoginTransaction = useCallback(async () => {
|
|
493
|
+
const current = projectionRef.current;
|
|
494
|
+
const activeTransaction = transactionRef.current;
|
|
495
|
+
if (!current || !activeTransaction) return;
|
|
496
|
+
const sequence = ++sequenceRef.current;
|
|
497
|
+
setPhase("committing");
|
|
498
|
+
const cancelOperation = cancelOperationRef.current ?? {
|
|
499
|
+
operationId: operationId(),
|
|
500
|
+
expectedGeneration: current.generation
|
|
501
|
+
};
|
|
502
|
+
cancelOperationRef.current = cancelOperation;
|
|
503
|
+
let next;
|
|
504
|
+
try {
|
|
505
|
+
next = await client.cancelLoginTransaction({
|
|
506
|
+
operationId: cancelOperation.operationId,
|
|
507
|
+
expectedGeneration: cancelOperation.expectedGeneration,
|
|
508
|
+
transactionId: activeTransaction.id
|
|
509
|
+
});
|
|
510
|
+
} catch (caught) {
|
|
511
|
+
if (sequenceRef.current !== sequence) {
|
|
512
|
+
if (cancelOperationRef.current?.operationId === cancelOperation.operationId) {
|
|
513
|
+
cancelOperationRef.current = null;
|
|
514
|
+
}
|
|
515
|
+
throw supersededOperationError();
|
|
516
|
+
}
|
|
517
|
+
await fail(caught, activeTransaction.kind);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (sequenceRef.current !== sequence) {
|
|
521
|
+
if (cancelOperationRef.current?.operationId === cancelOperation.operationId) {
|
|
522
|
+
cancelOperationRef.current = null;
|
|
523
|
+
}
|
|
524
|
+
throw supersededOperationError();
|
|
525
|
+
}
|
|
526
|
+
clearLoginTransactionState();
|
|
527
|
+
setProjection(next);
|
|
528
|
+
setPhase("ready");
|
|
529
|
+
setError(null);
|
|
530
|
+
}, [clearLoginTransactionState, client, fail]);
|
|
531
|
+
const selectSlot = useCallback(
|
|
532
|
+
async (slotId) => {
|
|
533
|
+
const id = operationId();
|
|
534
|
+
return await executePending({
|
|
535
|
+
kind: "select",
|
|
536
|
+
operationId: id,
|
|
537
|
+
expectedGeneration: null,
|
|
538
|
+
changesActor: (current) => current.selectedSlotId !== slotId,
|
|
539
|
+
execute: async (expectedGeneration) => await client.selectLoginSlot({
|
|
540
|
+
operationId: id,
|
|
541
|
+
expectedGeneration,
|
|
542
|
+
slotId
|
|
543
|
+
})
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
[client, executePending]
|
|
547
|
+
);
|
|
548
|
+
const logoutSlot = useCallback(
|
|
549
|
+
async (slotId, replacementSlotId) => {
|
|
550
|
+
const id = operationId();
|
|
551
|
+
return await executePending({
|
|
552
|
+
kind: "logout_one",
|
|
553
|
+
operationId: id,
|
|
554
|
+
expectedGeneration: null,
|
|
555
|
+
changesActor: (current) => current.selectedSlotId === slotId,
|
|
556
|
+
execute: async (expectedGeneration) => await client.logoutLoginSlot({
|
|
557
|
+
operationId: id,
|
|
558
|
+
expectedGeneration,
|
|
559
|
+
slotId,
|
|
560
|
+
replacementSlotId
|
|
561
|
+
})
|
|
562
|
+
});
|
|
563
|
+
},
|
|
564
|
+
[client, executePending]
|
|
565
|
+
);
|
|
566
|
+
const logoutAll = useCallback(async () => {
|
|
567
|
+
const id = operationId();
|
|
568
|
+
return await executePending({
|
|
569
|
+
kind: "logout_all",
|
|
570
|
+
operationId: id,
|
|
571
|
+
expectedGeneration: null,
|
|
572
|
+
// Logout-all always rotates the browser-set authority, even when the set
|
|
573
|
+
// is neutral but still contains slots. Peers must therefore enter the
|
|
574
|
+
// same precommit hold and reconciliation boundary unconditionally.
|
|
575
|
+
changesActor: () => true,
|
|
576
|
+
execute: async (expectedGeneration) => {
|
|
577
|
+
await client.logoutSessionSet({ operationId: id, expectedGeneration });
|
|
578
|
+
return await client.getSessionSet();
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}, [client, executePending]);
|
|
582
|
+
const resolveDeepLink = useCallback(
|
|
583
|
+
async (path) => await client.resolveDeepLink({ path }),
|
|
584
|
+
[client]
|
|
585
|
+
);
|
|
586
|
+
useEffect(() => {
|
|
587
|
+
const sequence = sequenceRef;
|
|
588
|
+
const transitionAbort = transitionAbortRef;
|
|
589
|
+
void refresh().catch((caught) => {
|
|
590
|
+
setError(accountError(caught));
|
|
591
|
+
setPhase("recoverable_error");
|
|
592
|
+
});
|
|
593
|
+
return () => {
|
|
594
|
+
++sequence.current;
|
|
595
|
+
transitionAbort.current?.abort();
|
|
596
|
+
};
|
|
597
|
+
}, [client, refresh]);
|
|
598
|
+
useEffect(() => {
|
|
599
|
+
if (!broadcastChannelName || typeof BroadcastChannel === "undefined") return;
|
|
600
|
+
const channel = new BroadcastChannel(broadcastChannelName);
|
|
601
|
+
const actorHolds = crossTabActorHoldsRef.current;
|
|
602
|
+
channelRef.current = channel;
|
|
603
|
+
channel.onmessage = (event) => {
|
|
604
|
+
const value2 = event.data;
|
|
605
|
+
if (!value2 || typeof value2 !== "object") return;
|
|
606
|
+
const hint = value2;
|
|
607
|
+
if (hint.type === "actor-transition-pending") {
|
|
608
|
+
if (typeof hint.transitionId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
|
|
609
|
+
hint.transitionId
|
|
610
|
+
) || typeof hint.actorEpoch !== "string" || typeof hint.generation !== "string" || crossTabActorHoldsRef.current.has(hint.transitionId)) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const current = projectionRef.current;
|
|
614
|
+
if (current && (current.actorEpoch !== hint.actorEpoch || current.generation !== hint.generation)) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (!current && crossTabActorHoldsRef.current.size === 0) return;
|
|
618
|
+
const firstHold = crossTabActorHoldsRef.current.size === 0;
|
|
619
|
+
const timeout = setTimeout(() => {
|
|
620
|
+
if (!crossTabActorHoldsRef.current.delete(hint.transitionId)) return;
|
|
621
|
+
if (crossTabActorHoldsRef.current.size === 0) {
|
|
622
|
+
void invalidateActor().catch(() => void 0);
|
|
623
|
+
}
|
|
624
|
+
}, CROSS_TAB_ACTOR_HOLD_MS);
|
|
625
|
+
crossTabActorHoldsRef.current.set(hint.transitionId, timeout);
|
|
626
|
+
if (firstHold) {
|
|
627
|
+
if (pendingCommitSequenceRef.current !== null) {
|
|
628
|
+
invalidatedDuringPendingCommitRef.current = true;
|
|
629
|
+
} else {
|
|
630
|
+
++sequenceRef.current;
|
|
631
|
+
}
|
|
632
|
+
void beginNeutralActorInvalidation(current).catch(() => void 0);
|
|
633
|
+
}
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (hint.type === "actor-transition-released") {
|
|
637
|
+
if (typeof hint.transitionId !== "string") return;
|
|
638
|
+
const timeout = crossTabActorHoldsRef.current.get(hint.transitionId);
|
|
639
|
+
if (timeout === void 0) return;
|
|
640
|
+
clearTimeout(timeout);
|
|
641
|
+
crossTabActorHoldsRef.current.delete(hint.transitionId);
|
|
642
|
+
if (crossTabActorHoldsRef.current.size === 0) {
|
|
643
|
+
void invalidateActor().catch(() => void 0);
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (hint.type !== "actor-epoch-changed" || typeof hint.actorEpoch !== "string" || typeof hint.generation !== "string" || hint.actorEpoch === projectionRef.current?.actorEpoch && hint.generation === projectionRef.current?.generation) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
for (const timeout of crossTabActorHoldsRef.current.values()) clearTimeout(timeout);
|
|
651
|
+
crossTabActorHoldsRef.current.clear();
|
|
652
|
+
void invalidateActor().catch(() => void 0);
|
|
653
|
+
};
|
|
654
|
+
return () => {
|
|
655
|
+
for (const timeout of actorHolds.values()) clearTimeout(timeout);
|
|
656
|
+
actorHolds.clear();
|
|
657
|
+
channel.close();
|
|
658
|
+
if (channelRef.current === channel) channelRef.current = null;
|
|
659
|
+
};
|
|
660
|
+
}, [beginNeutralActorInvalidation, broadcastChannelName, invalidateActor]);
|
|
661
|
+
const value = useMemo(
|
|
662
|
+
() => ({
|
|
663
|
+
projection,
|
|
664
|
+
phase,
|
|
665
|
+
blockers,
|
|
666
|
+
error,
|
|
667
|
+
transaction,
|
|
668
|
+
hasPendingTransition: pendingRef.current !== null,
|
|
669
|
+
refresh,
|
|
670
|
+
invalidateActor,
|
|
671
|
+
registerTransitionBlocker,
|
|
672
|
+
continueTransition,
|
|
673
|
+
cancelPendingTransition,
|
|
674
|
+
beginAdd,
|
|
675
|
+
beginReauth,
|
|
676
|
+
completeEmailPassword,
|
|
677
|
+
settleExternalLoginTransaction,
|
|
678
|
+
cancelLoginTransaction,
|
|
679
|
+
selectSlot,
|
|
680
|
+
logoutSlot,
|
|
681
|
+
logoutAll,
|
|
682
|
+
resolveDeepLink
|
|
683
|
+
}),
|
|
684
|
+
[
|
|
685
|
+
projection,
|
|
686
|
+
phase,
|
|
687
|
+
blockers,
|
|
688
|
+
error,
|
|
689
|
+
transaction,
|
|
690
|
+
refresh,
|
|
691
|
+
invalidateActor,
|
|
692
|
+
registerTransitionBlocker,
|
|
693
|
+
continueTransition,
|
|
694
|
+
cancelPendingTransition,
|
|
695
|
+
beginAdd,
|
|
696
|
+
beginReauth,
|
|
697
|
+
completeEmailPassword,
|
|
698
|
+
settleExternalLoginTransaction,
|
|
699
|
+
cancelLoginTransaction,
|
|
700
|
+
selectSlot,
|
|
701
|
+
logoutSlot,
|
|
702
|
+
logoutAll,
|
|
703
|
+
resolveDeepLink
|
|
704
|
+
]
|
|
705
|
+
);
|
|
706
|
+
return /* @__PURE__ */ jsx(BrowserAccountsContext.Provider, { value, children });
|
|
707
|
+
}
|
|
708
|
+
function useBrowserAccounts() {
|
|
709
|
+
const context = useContext(BrowserAccountsContext);
|
|
710
|
+
if (!context) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
"@opengeni/react/accounts: wrap the tree in <BrowserAccountsProvider> before using account hooks"
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
return context;
|
|
716
|
+
}
|
|
717
|
+
function useOptionalBrowserAccounts() {
|
|
718
|
+
return useContext(BrowserAccountsContext);
|
|
719
|
+
}
|
|
720
|
+
function useBrowserAccountTransitionBlocker(id, inspect) {
|
|
721
|
+
const { registerTransitionBlocker } = useBrowserAccounts();
|
|
722
|
+
const inspectRef = useRef(inspect);
|
|
723
|
+
inspectRef.current = inspect;
|
|
724
|
+
useEffect(
|
|
725
|
+
() => registerTransitionBlocker(id, () => inspectRef.current()),
|
|
726
|
+
[id, registerTransitionBlocker]
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
function useOptionalBrowserAccountTransitionBlocker(id, inspect) {
|
|
730
|
+
const context = useContext(BrowserAccountsContext);
|
|
731
|
+
const registerTransitionBlocker = context?.registerTransitionBlocker;
|
|
732
|
+
const inspectRef = useRef(inspect);
|
|
733
|
+
inspectRef.current = inspect;
|
|
734
|
+
useEffect(() => {
|
|
735
|
+
if (!registerTransitionBlocker) return;
|
|
736
|
+
return registerTransitionBlocker(id, () => inspectRef.current());
|
|
737
|
+
}, [id, registerTransitionBlocker]);
|
|
738
|
+
}
|
|
739
|
+
export {
|
|
740
|
+
BrowserAccountsProvider,
|
|
741
|
+
useBrowserAccountTransitionBlocker,
|
|
742
|
+
useBrowserAccounts,
|
|
743
|
+
useOptionalBrowserAccountTransitionBlocker,
|
|
744
|
+
useOptionalBrowserAccounts
|
|
745
|
+
};
|
|
746
|
+
//# sourceMappingURL=accounts.js.map
|