@turingfocus/chat-gateway-tfrobot 0.6.0 → 0.7.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/dist/dto.d.ts +14 -1
- package/dist/dto.d.ts.map +1 -1
- package/dist/dto.js +43 -6
- package/dist/dto.js.map +1 -1
- package/dist/gateway.d.ts +3 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +268 -19
- package/dist/gateway.js.map +1 -1
- package/dist/http.d.ts +5 -2
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +30 -23
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/mapper.d.ts.map +1 -1
- package/dist/mapper.js +4 -2
- package/dist/mapper.js.map +1 -1
- package/dist/socket.d.ts +21 -3
- package/dist/socket.d.ts.map +1 -1
- package/dist/socket.js +284 -33
- package/dist/socket.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
package/dist/socket.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { io } from "socket.io-client";
|
|
2
|
-
import { chatErrorSchema, chatUpdateSchema, createGatewayDeadlineExceededError, isGatewayDeadlineExceeded, } from "@turingfocus/chat-protocol";
|
|
2
|
+
import { chatErrorSchema, chatUpdateSchema, createGatewayDeadlineExceededError, getTimelineItemKey, isGatewayDeadlineExceeded, } from "@turingfocus/chat-protocol";
|
|
3
3
|
import { awaitBounded } from "./bounded.js";
|
|
4
4
|
import { chatErrorEventDtoSchema, eventDtoSchema, messageDtoSchema, socketProtocolErrorDtoSchema, stateChangedDtoSchema, statusDtoSchema, } from "./dto.js";
|
|
5
5
|
import { mapEventUpdate, mapMessageUpdate, mapRun, mapUnknownSocketEvent, } from "./mapper.js";
|
|
6
6
|
import { sanitizeCredentialError, sanitizeCredentialRaw, sanitizeCredentialText, } from "./redaction.js";
|
|
7
|
-
import { isValidTFRobotSession } from "./types.js";
|
|
7
|
+
import { isValidTFRobotSession, resolveTFRobotServerProfile } from "./types.js";
|
|
8
8
|
const KNOWN_EVENTS = new Set([
|
|
9
9
|
"chat_error",
|
|
10
10
|
"chat_event",
|
|
@@ -18,17 +18,21 @@ const KNOWN_EVENTS = new Set([
|
|
|
18
18
|
const MAX_TIMER_DELAY = 2_147_483_647;
|
|
19
19
|
const RECONNECT_AUTH_TIMEOUT_MS = 10_000;
|
|
20
20
|
const RECONNECT_JOIN_TIMEOUT_MS = 10_000;
|
|
21
|
-
const parseJoinAcknowledgement = (value, reconnect) => {
|
|
21
|
+
const parseJoinAcknowledgement = (value, reconnect, acceptEmpty) => {
|
|
22
22
|
if (value === undefined) {
|
|
23
23
|
return {
|
|
24
|
-
accepted: reconnect,
|
|
24
|
+
accepted: reconnect || acceptEmpty,
|
|
25
|
+
empty: true,
|
|
25
26
|
recoveryComplete: false,
|
|
26
|
-
...(reconnect
|
|
27
|
+
...(reconnect || acceptEmpty
|
|
28
|
+
? {}
|
|
29
|
+
: { rejectionCode: "validation" }),
|
|
27
30
|
};
|
|
28
31
|
}
|
|
29
32
|
if (value === null) {
|
|
30
33
|
return {
|
|
31
34
|
accepted: false,
|
|
35
|
+
empty: false,
|
|
32
36
|
recoveryComplete: false,
|
|
33
37
|
rejectionCode: "validation",
|
|
34
38
|
};
|
|
@@ -36,16 +40,18 @@ const parseJoinAcknowledgement = (value, reconnect) => {
|
|
|
36
40
|
if (value === false) {
|
|
37
41
|
return {
|
|
38
42
|
accepted: false,
|
|
43
|
+
empty: false,
|
|
39
44
|
recoveryComplete: false,
|
|
40
45
|
rejectionCode: "validation",
|
|
41
46
|
};
|
|
42
47
|
}
|
|
43
48
|
if (value === true) {
|
|
44
|
-
return { accepted: true, recoveryComplete: !reconnect };
|
|
49
|
+
return { accepted: true, empty: false, recoveryComplete: !reconnect };
|
|
45
50
|
}
|
|
46
51
|
if (typeof value !== "object") {
|
|
47
52
|
return {
|
|
48
53
|
accepted: false,
|
|
54
|
+
empty: false,
|
|
49
55
|
recoveryComplete: false,
|
|
50
56
|
rejectionCode: "validation",
|
|
51
57
|
};
|
|
@@ -120,6 +126,7 @@ const parseJoinAcknowledgement = (value, reconnect) => {
|
|
|
120
126
|
const authorizationRejected = statuses.some(({ normalized, numeric }) => numeric === 403 || normalized === "forbidden") || normalizedError === "forbidden";
|
|
121
127
|
return {
|
|
122
128
|
accepted,
|
|
129
|
+
empty: false,
|
|
123
130
|
...(authenticationRejected
|
|
124
131
|
? { rejectionCode: "authentication" }
|
|
125
132
|
: authorizationRejected
|
|
@@ -218,27 +225,65 @@ export const createSocketIoFactoryWith = (connect) => ({ getAuth, namespaceUrl,
|
|
|
218
225
|
},
|
|
219
226
|
});
|
|
220
227
|
export const createSocketIoFactory = createSocketIoFactoryWith(io);
|
|
228
|
+
const MAX_RECOVERY_IDENTITIES = 10_000;
|
|
229
|
+
const rememberBoundedSetValue = (values, value) => {
|
|
230
|
+
values.delete(value);
|
|
231
|
+
values.add(value);
|
|
232
|
+
while (values.size > MAX_RECOVERY_IDENTITIES) {
|
|
233
|
+
const oldest = values.values().next().value;
|
|
234
|
+
if (oldest === undefined)
|
|
235
|
+
break;
|
|
236
|
+
values.delete(oldest);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const rememberBoundedMapValue = (values, key, value) => {
|
|
240
|
+
values.delete(key);
|
|
241
|
+
values.set(key, value);
|
|
242
|
+
while (values.size > MAX_RECOVERY_IDENTITIES) {
|
|
243
|
+
const oldest = values.keys().next().value;
|
|
244
|
+
if (oldest === undefined)
|
|
245
|
+
break;
|
|
246
|
+
values.delete(oldest);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
export const recoveryIdentityOfUpdate = (update) => {
|
|
250
|
+
if (update.kind === "timeline.upsert") {
|
|
251
|
+
return getTimelineItemKey(update.item);
|
|
252
|
+
}
|
|
253
|
+
if (update.kind === "event.transition.upsert") {
|
|
254
|
+
return `event:${update.event.id}:transition:${update.event.transition.id}`;
|
|
255
|
+
}
|
|
256
|
+
return undefined;
|
|
257
|
+
};
|
|
258
|
+
const recoveryIdentitiesOfSnapshot = (snapshot) => snapshot.timeline.flatMap((item) => item.kind === "agent-event"
|
|
259
|
+
? item.transitions.map((transition) => `event:${item.id}:transition:${transition.id}`)
|
|
260
|
+
: [getTimelineItemKey(item)]);
|
|
221
261
|
export class TFRobotSocketClient {
|
|
222
262
|
#disposed = false;
|
|
223
|
-
#establishmentAbort;
|
|
224
263
|
#establishing;
|
|
264
|
+
#pendingEstablishment;
|
|
225
265
|
#factory;
|
|
226
266
|
#namespaceUrl;
|
|
227
267
|
#now;
|
|
228
268
|
#options;
|
|
229
269
|
#path;
|
|
270
|
+
#profile;
|
|
271
|
+
#currentServerRestLoader;
|
|
230
272
|
#reconnectStatusLoader;
|
|
273
|
+
#recoveryCheckpoints = new Map();
|
|
231
274
|
#runs = new Map();
|
|
232
275
|
#subscriptions = new Set();
|
|
233
276
|
#subscriptionGeneration = 0;
|
|
234
|
-
constructor(options, reconnectStatusLoader) {
|
|
277
|
+
constructor(options, reconnectStatusLoader, currentServerRestLoader) {
|
|
235
278
|
this.#options = options;
|
|
236
279
|
this.#factory = options.socketFactory ?? createSocketIoFactory;
|
|
237
280
|
this.#path = options.socketPath ?? "/socket.io";
|
|
238
281
|
this.#now = options.now ?? Date.now;
|
|
282
|
+
this.#profile = resolveTFRobotServerProfile(options.serverProfile);
|
|
239
283
|
this.#namespaceUrl =
|
|
240
284
|
options.socketNamespaceUrl ?? this.#defaultNamespaceUrl(options.baseUrl);
|
|
241
285
|
this.#reconnectStatusLoader = reconnectStatusLoader;
|
|
286
|
+
this.#currentServerRestLoader = currentServerRestLoader;
|
|
242
287
|
}
|
|
243
288
|
async subscribe(conversationId, options, observer) {
|
|
244
289
|
if (this.#disposed) {
|
|
@@ -257,14 +302,25 @@ export class TFRobotSocketClient {
|
|
|
257
302
|
const subscriptionId = `tfrobot-subscription-${generation}`;
|
|
258
303
|
this.#cancelEstablishment();
|
|
259
304
|
const establishmentAbort = new AbortController();
|
|
260
|
-
|
|
261
|
-
|
|
305
|
+
const pendingEstablishment = {
|
|
306
|
+
abort: establishmentAbort,
|
|
307
|
+
conversationId,
|
|
308
|
+
generation,
|
|
309
|
+
};
|
|
310
|
+
this.#pendingEstablishment = pendingEstablishment;
|
|
311
|
+
const clearPendingEstablishment = () => {
|
|
312
|
+
if (this.#pendingEstablishment === pendingEstablishment) {
|
|
313
|
+
this.#pendingEstablishment = undefined;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
const authOutcome = await awaitBounded(() => this.#getSocketSession(conversationId, "connect"), {
|
|
262
317
|
deadlineAt: options.deadlineAt,
|
|
263
318
|
now: this.#now,
|
|
264
319
|
signal: establishmentAbort.signal,
|
|
265
320
|
});
|
|
266
321
|
switch (authOutcome.kind) {
|
|
267
322
|
case "aborted": {
|
|
323
|
+
clearPendingEstablishment();
|
|
268
324
|
return {
|
|
269
325
|
ok: false,
|
|
270
326
|
error: this.#error("conflict", this.#disposed
|
|
@@ -273,12 +329,14 @@ export class TFRobotSocketClient {
|
|
|
273
329
|
};
|
|
274
330
|
}
|
|
275
331
|
case "deadline": {
|
|
332
|
+
clearPendingEstablishment();
|
|
276
333
|
return {
|
|
277
334
|
ok: false,
|
|
278
335
|
error: createGatewayDeadlineExceededError(),
|
|
279
336
|
};
|
|
280
337
|
}
|
|
281
338
|
case "error": {
|
|
339
|
+
clearPendingEstablishment();
|
|
282
340
|
return {
|
|
283
341
|
ok: false,
|
|
284
342
|
error: this.#error("authentication", "Unable to obtain a TFRobot Socket session", true, undefined),
|
|
@@ -289,14 +347,43 @@ export class TFRobotSocketClient {
|
|
|
289
347
|
}
|
|
290
348
|
}
|
|
291
349
|
if (this.#disposed || establishmentAbort.signal.aborted) {
|
|
350
|
+
clearPendingEstablishment();
|
|
292
351
|
return {
|
|
293
352
|
ok: false,
|
|
294
353
|
error: this.#error("conflict", "TFRobot Gateway was disposed during subscription", false, undefined),
|
|
295
354
|
};
|
|
296
355
|
}
|
|
297
|
-
const
|
|
356
|
+
const initialSession = authOutcome.value;
|
|
357
|
+
if (this.#profile.kind === "current-server") {
|
|
358
|
+
const preflight = await this.#currentServerRestLoader({
|
|
359
|
+
checkpoint: new Set(),
|
|
360
|
+
conversationId,
|
|
361
|
+
deadlineAt: options.deadlineAt,
|
|
362
|
+
limits: {
|
|
363
|
+
...this.#profile.rebase,
|
|
364
|
+
maxItems: this.#profile.rebase.pageSize,
|
|
365
|
+
maxPages: 1,
|
|
366
|
+
},
|
|
367
|
+
session: initialSession,
|
|
368
|
+
signal: establishmentAbort.signal,
|
|
369
|
+
});
|
|
370
|
+
if (this.#disposed || establishmentAbort.signal.aborted) {
|
|
371
|
+
clearPendingEstablishment();
|
|
372
|
+
return {
|
|
373
|
+
ok: false,
|
|
374
|
+
error: this.#error("conflict", "TFRobot subscription was replaced during REST preflight", false, undefined),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (!preflight.ok) {
|
|
378
|
+
clearPendingEstablishment();
|
|
379
|
+
return preflight;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const initialAuth = authOf(initialSession);
|
|
383
|
+
const credentialValues = new Set(Object.values(initialAuth));
|
|
298
384
|
const errorConversationId = () => sanitizeCredentialText(conversationId, credentialValues);
|
|
299
|
-
let firstAuth =
|
|
385
|
+
let firstAuth = initialAuth;
|
|
386
|
+
let currentServerSession = initialSession;
|
|
300
387
|
let authenticationFailure;
|
|
301
388
|
const connectionAbort = new AbortController();
|
|
302
389
|
let socket;
|
|
@@ -313,20 +400,22 @@ export class TFRobotSocketClient {
|
|
|
313
400
|
firstAuth = undefined;
|
|
314
401
|
return auth;
|
|
315
402
|
}
|
|
316
|
-
const
|
|
403
|
+
const reconnectSession = await awaitBounded(() => this.#getSocketSession(conversationId, "reconnect"), {
|
|
317
404
|
deadlineAt: this.#now() + RECONNECT_AUTH_TIMEOUT_MS,
|
|
318
405
|
now: this.#now,
|
|
319
406
|
signal: connectionAbort.signal,
|
|
320
407
|
});
|
|
321
|
-
if (
|
|
322
|
-
|
|
408
|
+
if (reconnectSession.kind === "value") {
|
|
409
|
+
currentServerSession = reconnectSession.value;
|
|
410
|
+
const reconnectAuth = authOf(reconnectSession.value);
|
|
411
|
+
for (const value of Object.values(reconnectAuth)) {
|
|
323
412
|
credentialValues.add(value);
|
|
324
413
|
}
|
|
325
|
-
return reconnectAuth
|
|
414
|
+
return reconnectAuth;
|
|
326
415
|
}
|
|
327
|
-
const reason =
|
|
328
|
-
?
|
|
329
|
-
: new Error(
|
|
416
|
+
const reason = reconnectSession.kind === "error"
|
|
417
|
+
? reconnectSession.reason
|
|
418
|
+
: new Error(reconnectSession.kind === "deadline"
|
|
330
419
|
? "TFRobot Socket session refresh exceeded its deadline"
|
|
331
420
|
: "TFRobot Socket session refresh was cancelled");
|
|
332
421
|
authenticationFailure = reason;
|
|
@@ -336,6 +425,7 @@ export class TFRobotSocketClient {
|
|
|
336
425
|
}
|
|
337
426
|
catch (reason) {
|
|
338
427
|
connectionAbort.abort();
|
|
428
|
+
clearPendingEstablishment();
|
|
339
429
|
return {
|
|
340
430
|
ok: false,
|
|
341
431
|
error: this.#transportError(reason, "Unable to create the TFRobot Socket transport", conversationId, credentialValues),
|
|
@@ -496,12 +586,18 @@ export class TFRobotSocketClient {
|
|
|
496
586
|
? {}
|
|
497
587
|
: {
|
|
498
588
|
recoveryComplete: recovery.complete,
|
|
589
|
+
...(recovery.assurance === undefined
|
|
590
|
+
? {}
|
|
591
|
+
: { recoveryAssurance: recovery.assurance }),
|
|
499
592
|
...(recovery.cursor === undefined
|
|
500
593
|
? {}
|
|
501
594
|
: { recoveryCursor: recovery.cursor }),
|
|
502
595
|
...(recovery.reason === undefined
|
|
503
596
|
? {}
|
|
504
597
|
: { recoveryReason: recovery.reason }),
|
|
598
|
+
...(recovery.source === undefined
|
|
599
|
+
? {}
|
|
600
|
+
: { recoverySource: recovery.source }),
|
|
505
601
|
}),
|
|
506
602
|
})).catch(() => undefined);
|
|
507
603
|
}
|
|
@@ -533,8 +629,74 @@ export class TFRobotSocketClient {
|
|
|
533
629
|
report(this.#error("validation", invalidMessage, false, errorConversationId()));
|
|
534
630
|
return;
|
|
535
631
|
}
|
|
632
|
+
const recoveryIdentity = recoveryIdentityOfUpdate(update);
|
|
633
|
+
if (recoveryIdentity !== undefined) {
|
|
634
|
+
active.realtimeRevision += 1;
|
|
635
|
+
rememberBoundedMapValue(active.realtimeIdentityRevisions, recoveryIdentity, active.realtimeRevision);
|
|
636
|
+
const checkpoint = this.#recoveryCheckpoints.get(conversationId) ?? new Set();
|
|
637
|
+
rememberBoundedSetValue(checkpoint, recoveryIdentity);
|
|
638
|
+
this.#recoveryCheckpoints.set(conversationId, checkpoint);
|
|
639
|
+
}
|
|
536
640
|
next(update);
|
|
537
641
|
};
|
|
642
|
+
const rebaseCurrentServer = async (recoveryRevision, runRevision, session, checkpoint, realtimeRevision) => {
|
|
643
|
+
if (this.#profile.kind !== "current-server") {
|
|
644
|
+
return { kind: "failure" };
|
|
645
|
+
}
|
|
646
|
+
const result = await this.#currentServerRestLoader({
|
|
647
|
+
checkpoint,
|
|
648
|
+
conversationId,
|
|
649
|
+
deadlineAt: this.#now() + this.#profile.rebase.deadlineMs,
|
|
650
|
+
limits: this.#profile.rebase,
|
|
651
|
+
session,
|
|
652
|
+
signal: connectionAbort.signal,
|
|
653
|
+
});
|
|
654
|
+
if (!active.active ||
|
|
655
|
+
!this.#subscriptions.has(active) ||
|
|
656
|
+
!active.socket.connected ||
|
|
657
|
+
active.recoveryRevision !== recoveryRevision) {
|
|
658
|
+
return { kind: "failure" };
|
|
659
|
+
}
|
|
660
|
+
if (!result.ok) {
|
|
661
|
+
const error = sanitizeCredentialError(result.error, credentialValues);
|
|
662
|
+
if (error.code === "authentication" || error.code === "authorization") {
|
|
663
|
+
report(error, "authentication");
|
|
664
|
+
return { kind: "auth-required" };
|
|
665
|
+
}
|
|
666
|
+
report(error, "recovery");
|
|
667
|
+
return { kind: "failure" };
|
|
668
|
+
}
|
|
669
|
+
const remembered = this.#recoveryCheckpoints.get(conversationId) ?? new Set();
|
|
670
|
+
for (const update of result.value.updates) {
|
|
671
|
+
const identity = recoveryIdentityOfUpdate(update);
|
|
672
|
+
if (identity !== undefined &&
|
|
673
|
+
(active.realtimeIdentityRevisions.get(identity) ?? 0) >
|
|
674
|
+
realtimeRevision) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
next(update);
|
|
678
|
+
if (identity !== undefined)
|
|
679
|
+
rememberBoundedSetValue(remembered, identity);
|
|
680
|
+
}
|
|
681
|
+
this.#recoveryCheckpoints.set(conversationId, remembered);
|
|
682
|
+
if (active.runRevision === runRevision) {
|
|
683
|
+
const previous = this.#runs.get(conversationId);
|
|
684
|
+
this.#runs.set(conversationId, result.value.run);
|
|
685
|
+
if (!sameRun(previous, result.value.run)) {
|
|
686
|
+
next(chatUpdateSchema.parse({
|
|
687
|
+
kind: "run.replace",
|
|
688
|
+
conversationId: errorConversationId(),
|
|
689
|
+
run: result.value.run,
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const reason = result.value.checkpointReached
|
|
694
|
+
? "rest-rebase-reached-checkpoint"
|
|
695
|
+
: result.value.boundedBy === undefined
|
|
696
|
+
? "rest-rebase-history-exhausted"
|
|
697
|
+
: `rest-rebase-${result.value.boundedBy}`;
|
|
698
|
+
return { kind: "success", reason };
|
|
699
|
+
};
|
|
538
700
|
let settleEstablishment;
|
|
539
701
|
add("connect", () => {
|
|
540
702
|
if (!active.active)
|
|
@@ -563,6 +725,8 @@ export class TFRobotSocketClient {
|
|
|
563
725
|
const reconnect = subscriptionEstablished;
|
|
564
726
|
if (reconnect)
|
|
565
727
|
active.reconnectAttempt += 1;
|
|
728
|
+
const rebaseCheckpoint = new Set(this.#recoveryCheckpoints.get(conversationId) ?? []);
|
|
729
|
+
const rebaseRealtimeRevision = active.realtimeRevision;
|
|
566
730
|
const recoveryRevision = ++active.recoveryRevision;
|
|
567
731
|
const runRevision = active.runRevision;
|
|
568
732
|
const joinStartedAt = this.#now();
|
|
@@ -589,11 +753,12 @@ export class TFRobotSocketClient {
|
|
|
589
753
|
const rawAcknowledgement = acknowledgementArguments[0];
|
|
590
754
|
acknowledgement = parseJoinAcknowledgement(rawAcknowledgement === undefined || rawAcknowledgement === true
|
|
591
755
|
? rawAcknowledgement
|
|
592
|
-
: sanitizeCredentialRaw(rawAcknowledgement, credentialValues), reconnect);
|
|
756
|
+
: sanitizeCredentialRaw(rawAcknowledgement, credentialValues), reconnect, this.#profile.kind === "current-server");
|
|
593
757
|
}
|
|
594
758
|
catch {
|
|
595
759
|
acknowledgement = {
|
|
596
760
|
accepted: false,
|
|
761
|
+
empty: false,
|
|
597
762
|
recoveryComplete: false,
|
|
598
763
|
rejectionCode: "validation",
|
|
599
764
|
message: "Invalid TFRobot conversation subscription acknowledgement",
|
|
@@ -661,12 +826,24 @@ export class TFRobotSocketClient {
|
|
|
661
826
|
});
|
|
662
827
|
resolveError("connection");
|
|
663
828
|
resolveError("authentication");
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
:
|
|
669
|
-
|
|
829
|
+
if (this.#profile.kind === "current-server" &&
|
|
830
|
+
acknowledgement.empty) {
|
|
831
|
+
lifecycle("degraded", {
|
|
832
|
+
assurance: "best-effort",
|
|
833
|
+
complete: false,
|
|
834
|
+
reason: "initial-rest-preflight",
|
|
835
|
+
source: "rest-rebase",
|
|
836
|
+
}, this.#now() - joinStartedAt);
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
lifecycle("active", {
|
|
840
|
+
complete: true,
|
|
841
|
+
...(acknowledgement.cursor === undefined
|
|
842
|
+
? {}
|
|
843
|
+
: { cursor: acknowledgement.cursor }),
|
|
844
|
+
}, this.#now() - joinStartedAt);
|
|
845
|
+
}
|
|
846
|
+
currentServerSession = undefined;
|
|
670
847
|
active.acceptingEvents = true;
|
|
671
848
|
flushRealtime();
|
|
672
849
|
return;
|
|
@@ -684,6 +861,48 @@ export class TFRobotSocketClient {
|
|
|
684
861
|
}, joinLatencyMs);
|
|
685
862
|
active.acceptingEvents = true;
|
|
686
863
|
flushRealtime();
|
|
864
|
+
if (this.#profile.kind === "current-server" &&
|
|
865
|
+
!acknowledgement.recoveryComplete) {
|
|
866
|
+
const recoverySession = currentServerSession;
|
|
867
|
+
currentServerSession = undefined;
|
|
868
|
+
if (recoverySession === undefined) {
|
|
869
|
+
report(this.#error("authentication", "TFRobot reconnect did not provide a session for REST rebase", true, errorConversationId()), "recovery");
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
void rebaseCurrentServer(recoveryRevision, runRevision, recoverySession, rebaseCheckpoint, rebaseRealtimeRevision)
|
|
873
|
+
.then((outcome) => {
|
|
874
|
+
if (!active.active ||
|
|
875
|
+
!this.#subscriptions.has(active) ||
|
|
876
|
+
!active.socket.connected ||
|
|
877
|
+
active.recoveryRevision !== recoveryRevision) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (outcome.kind === "auth-required") {
|
|
881
|
+
enterAuthRequired();
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (outcome.kind !== "success")
|
|
885
|
+
return;
|
|
886
|
+
resolveError("connection");
|
|
887
|
+
resolveError("recovery");
|
|
888
|
+
resolveError("authentication");
|
|
889
|
+
active.manualReconnectAttempted = false;
|
|
890
|
+
active.manualReconnectPending = false;
|
|
891
|
+
active.authRequired = false;
|
|
892
|
+
active.terminal = false;
|
|
893
|
+
lifecycle("degraded", {
|
|
894
|
+
assurance: "best-effort",
|
|
895
|
+
complete: false,
|
|
896
|
+
reason: outcome.reason,
|
|
897
|
+
source: "rest-rebase",
|
|
898
|
+
}, joinLatencyMs);
|
|
899
|
+
})
|
|
900
|
+
.catch(() => {
|
|
901
|
+
report(this.#error("unknown", "TFRobot REST rebase failed unexpectedly", true, errorConversationId()), "recovery");
|
|
902
|
+
});
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
currentServerSession = undefined;
|
|
687
906
|
void this.#reconcileRunAfterReconnect(active, conversationId, credentialValues, recoveryRevision, runRevision, next, report)
|
|
688
907
|
.then((outcome) => {
|
|
689
908
|
if (outcome === "auth-required") {
|
|
@@ -987,6 +1206,7 @@ export class TFRobotSocketClient {
|
|
|
987
1206
|
// Adapter ownership is cleared even if an injected transport misbehaves.
|
|
988
1207
|
}
|
|
989
1208
|
firstAuth = undefined;
|
|
1209
|
+
currentServerSession = undefined;
|
|
990
1210
|
credentialValues.clear();
|
|
991
1211
|
pendingNotifications.length = 0;
|
|
992
1212
|
pendingRealtimeActions.length = 0;
|
|
@@ -994,6 +1214,7 @@ export class TFRobotSocketClient {
|
|
|
994
1214
|
if (setupFailure !== undefined) {
|
|
995
1215
|
const error = this.#transportError(setupFailure.reason, "Unable to configure the TFRobot Socket transport", conversationId, credentialValues);
|
|
996
1216
|
cleanup();
|
|
1217
|
+
clearPendingEstablishment();
|
|
997
1218
|
return {
|
|
998
1219
|
ok: false,
|
|
999
1220
|
error,
|
|
@@ -1016,6 +1237,8 @@ export class TFRobotSocketClient {
|
|
|
1016
1237
|
manualReconnectPending: false,
|
|
1017
1238
|
observer,
|
|
1018
1239
|
reconnectAttempt: 0,
|
|
1240
|
+
realtimeIdentityRevisions: new Map(),
|
|
1241
|
+
realtimeRevision: 0,
|
|
1019
1242
|
recoveryRevision: 0,
|
|
1020
1243
|
runRevision: 0,
|
|
1021
1244
|
socket,
|
|
@@ -1031,9 +1254,7 @@ export class TFRobotSocketClient {
|
|
|
1031
1254
|
return;
|
|
1032
1255
|
settled = true;
|
|
1033
1256
|
settleEstablishment = undefined;
|
|
1034
|
-
|
|
1035
|
-
this.#establishmentAbort = undefined;
|
|
1036
|
-
}
|
|
1257
|
+
clearPendingEstablishment();
|
|
1037
1258
|
if (this.#establishing === active)
|
|
1038
1259
|
this.#establishing = undefined;
|
|
1039
1260
|
clearTimeout(timeout);
|
|
@@ -1087,16 +1308,46 @@ export class TFRobotSocketClient {
|
|
|
1087
1308
|
subscription.cleanup();
|
|
1088
1309
|
}
|
|
1089
1310
|
this.#subscriptions.clear();
|
|
1311
|
+
this.#recoveryCheckpoints.clear();
|
|
1090
1312
|
this.#runs.clear();
|
|
1091
1313
|
}
|
|
1314
|
+
rememberSnapshot(snapshot) {
|
|
1315
|
+
if (this.#disposed)
|
|
1316
|
+
return;
|
|
1317
|
+
const conversationId = snapshot.conversation.id;
|
|
1318
|
+
this.#runs.set(conversationId, snapshot.run);
|
|
1319
|
+
const checkpoint = this.#recoveryCheckpoints.get(conversationId) ?? new Set();
|
|
1320
|
+
for (const identity of recoveryIdentitiesOfSnapshot(snapshot)) {
|
|
1321
|
+
rememberBoundedSetValue(checkpoint, identity);
|
|
1322
|
+
}
|
|
1323
|
+
this.#recoveryCheckpoints.set(conversationId, checkpoint);
|
|
1324
|
+
}
|
|
1092
1325
|
rememberRun(conversationId, run) {
|
|
1093
1326
|
if (this.#disposed)
|
|
1094
1327
|
return;
|
|
1095
1328
|
this.#runs.set(conversationId, run);
|
|
1096
1329
|
}
|
|
1330
|
+
forgetConversation(conversationId) {
|
|
1331
|
+
if (this.#disposed)
|
|
1332
|
+
return;
|
|
1333
|
+
if (this.#pendingEstablishment?.conversationId === conversationId ||
|
|
1334
|
+
this.#establishing?.conversationId === conversationId) {
|
|
1335
|
+
this.#cancelEstablishment();
|
|
1336
|
+
}
|
|
1337
|
+
for (const subscription of [...this.#subscriptions]) {
|
|
1338
|
+
if (subscription.conversationId !== conversationId)
|
|
1339
|
+
continue;
|
|
1340
|
+
subscription.active = false;
|
|
1341
|
+
subscription.recoveryRevision += 1;
|
|
1342
|
+
subscription.cleanup();
|
|
1343
|
+
this.#subscriptions.delete(subscription);
|
|
1344
|
+
}
|
|
1345
|
+
this.#recoveryCheckpoints.delete(conversationId);
|
|
1346
|
+
this.#runs.delete(conversationId);
|
|
1347
|
+
}
|
|
1097
1348
|
#cancelEstablishment() {
|
|
1098
|
-
this.#
|
|
1099
|
-
this.#
|
|
1349
|
+
this.#pendingEstablishment?.abort.abort();
|
|
1350
|
+
this.#pendingEstablishment = undefined;
|
|
1100
1351
|
this.#establishing?.cancelEstablishment();
|
|
1101
1352
|
this.#establishing = undefined;
|
|
1102
1353
|
}
|
|
@@ -1107,7 +1358,7 @@ export class TFRobotSocketClient {
|
|
|
1107
1358
|
url.hash = "";
|
|
1108
1359
|
return url.toString().replace(/\/$/u, "");
|
|
1109
1360
|
}
|
|
1110
|
-
async #
|
|
1361
|
+
async #getSocketSession(conversationId, purpose) {
|
|
1111
1362
|
const session = await this.#options.sessionProvider.getSession({
|
|
1112
1363
|
purpose,
|
|
1113
1364
|
operation: "subscribe",
|
|
@@ -1116,7 +1367,7 @@ export class TFRobotSocketClient {
|
|
|
1116
1367
|
if (!isValidTFRobotSession(session)) {
|
|
1117
1368
|
throw new TypeError("SessionProvider returned invalid TFRobot credentials");
|
|
1118
1369
|
}
|
|
1119
|
-
return
|
|
1370
|
+
return session;
|
|
1120
1371
|
}
|
|
1121
1372
|
async #reconcileRunAfterReconnect(active, conversationId, credentialValues, recoveryRevision, runRevision, next, report) {
|
|
1122
1373
|
const result = await this.#reconnectStatusLoader(conversationId);
|